简体   繁体   中英

static asset serving from absolute path in play framework 2.3.x

I need to serve image files from an absolute path that is not on the classpath. When I use Assets.at(path, file) , it only searches inside /assets . I have mapped the url onto a controller function like the following:

public static Action<AnyContent> getImage(String imageId) {
    String path = PICTURE_UPLOAD_DIR; // here this path is absolute
    String file = imageId + ".png";
    return Assets.at(path, file);
}

How can I make this work?

NOTE: The reason to make images served using Assets is because of the auto etagging feature that make easy to send http 304 not modified. It seems that there is no auto etagging feature that play provides independently from Assets

Assets.at() works only for assets added to the classpath at build-time. See: https://www.playframework.com/documentation/2.4.x/Assets

The solution would be to read the files from the disk as byte[] then return the byte[] in the response body.

Converting the image to byte[] (this solution is for small files only, for large files look into streams):

private static Promise<byte[]> toBytes(final File file) {
    return Promise.promise(new Function0<byte[]>() {
        @Override
        public byte[] apply() throws Throwable {
            byte[] buffer = new byte[1024];
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            FileInputStream is = new FileInputStream(file);

            for (int readNum; (readNum = is.read(buffer)) != -1;) {
                os.write(buffer, 0, readNum);
            }
            return os.toByteArray();
        }
    });
}

The controller that uses toBytes() to serve the image:

public static Promise<Result> img() {
    //path is sent as JSON in request body
    JsonNode path = request().body().asJson();

    Logger.debug("path -> " + path.get("path").asText());
    Path p = Paths.get(path.get("path").asText());
    File file = new File(path.get("path").asText());

    try {
        response().setHeader("Content-Type", Files.probeContentType(p));
    } catch (IOException e) {
        Logger.error("BUMMER!", e);
        return Promise.promise(new Function0<Result>() {
            @Override
            public Result apply() throws Throwable {
                return badRequest();
            }
        });
    }

    return toBytes(file).map(new Function<byte[], Result>() {
        @Override
        public Result apply(byte[] bytes) throws Throwable {
            return ok(bytes);
        }       
    }).recover(new Function<Throwable, Result>() {
        @Override
        public Result apply(Throwable t) throws Throwable {
            return badRequest(t.getMessage());
        }
    });
}

The route:

POST    /img    controllers.YourControllerName.img()


If ETag support is needed:

(not adding Date or Last-Modified headers as they are not needed if ETag header is used instead):

Get SHA1 for the file:

private static Promise<String> toSHA1(final byte[] bytes) {       
    return Promise.promise(new Function0<String>() {
        @Override
        public String apply() throws Throwable {
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            byte[] digestResult = digest.digest(bytes);
            String hexResult = "";
            for (int i = 0; i < digestResult.length; i++) {
                hexResult += Integer.toString(( bytes[i] & 0xff ) + 0x100, 16).substring(1);
            }
            return hexResult;
        }
    });
}

Setting the ETag headers:

private static boolean setETagHeaders(String etag, String mime) {
    response().setHeader("Cache-Control", "no-cache");
    response().setHeader("ETag", "\"" + etag + "\"");
    boolean ifNoneMatch = false;

    if (request().hasHeader(IF_NONE_MATCH)) {
        String header = request().getHeader(IF_NONE_MATCH);
        //removing ""
        if (!etag.equals(header.substring(1, header.length() - 1))) {
            response().setHeader(CONTENT_TYPE, mime);
        } 
        ifNoneMatch = true;
    } else {
        response().setHeader(CONTENT_TYPE, mime);
    }
    return ifNoneMatch;
}

Controller with ETag support:

public static Promise<Result> img() {
    //path is sent as JSON in request body
    JsonNode path = request().body().asJson();
    Logger.debug("path -> " + path.get("path").asText());
    Path p = Paths.get(path.get("path").asText());
    File file = new File(path.get("path").asText());        
    final String mime;

    try {
        mime = Files.probeContentType(p);            
    } catch (IOException e) {
        Logger.error("BUMMER!", e);
        return Promise.promise(new Function0<Result>() {
            @Override
            public Result apply() throws Throwable {
                return badRequest();
            }
        });
    }
    return toBytes(file).flatMap(new Function<byte[], Promise<Result>>() {
        @Override
        public Promise<Result> apply(final byte[] bytes) throws Throwable {
            return toSHA1(bytes).map(new Function<String, Result>() {
                @Override
                public Result apply(String sha1) throws Throwable {
                    if (setETagHeaders(sha1, mime)) {
                        return status(304);
                    }
                    return ok(bytes);
                }
            });
        }
    }).recover(new Function<Throwable, Result>() {
        @Override
        public Result apply(Throwable t) throws Throwable {
            return badRequest(t.getMessage());
        }
    });
}



A few drawbacks(there's always a BUT):

  1. This is blocking. So it's better to execute it on another Akka thread-pool configured for blocking IO.
  2. As mentioned, the conversion to byte[] is for small files only, as it uses the memory for buffering. This should not be a problem in the case where you only serve small files(think web site grade images). See: http://docs.oracle.com/javase/tutorial/essential/io/file.html for different ways to read files using NIO2.

I've managed to solve this problem in a simpler way:

public static Result image(String image) {
  String basePath = "/opt/myapp/images";

  Path path = Paths.get(basePath + File.separator + image);
  Logger.info("External image::" + path);
  File file = path.toFile();
  if(file.exists()) {
    return ok(file);
  } else {
    String fallbackImage = "/assets/images/myimage.jpg";
    return redirect(fallbackImage);
  }
}

Route example:

GET     /image/:file    controllers.ExternalImagesController.image(file: String)

For large image files, you can use streaming. Official docs can help you on that way.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM