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

爱⌒轻易说出口 提交于 2019-12-05 07:27:45

问题


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


回答1:


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.



回答2:


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.



来源:https://stackoverflow.com/questions/28827515/static-asset-serving-from-absolute-path-in-play-framework-2-3-x

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!