从播放框架 2 中的绝对路径提供的静态资产。3.x
static asset serving from absolute path in play framework 2.3.x
我需要从不在类路径中的绝对路径提供图像文件。当我使用 Assets.at(path, file)
时,它只在 /assets
内搜索。我已将 url 映射到控制器函数上,如下所示:
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);
}
我怎样才能使这个工作?
注意:使用 Assets
提供图像的原因是因为自动标记功能可以轻松发送未修改的 http 304。似乎没有 play 独立于 Assets
提供的自动标记功能
Assets.at() 仅适用于添加到类路径 build-time 的资产。
参见:https://www.playframework.com/documentation/2.4.x/Assets
解决方案是从磁盘读取文件作为 byte[],然后 return 响应中的 byte[] body。
将图像转换为字节[](此解决方案仅适用于小文件,对于大文件请查看流):
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();
}
});
}
使用toBytes() 服务图像的控制器:
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());
}
});
}
路线:
POST /img controllers.YourControllerName.img()
如果需要 ETag 支持:
(不添加日期或 Last-Modified headers,因为如果使用 ETag header 则不需要它们):
获取文件的 SHA1:
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;
}
});
}
设置 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;
}
支持 ETag 的控制器:
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());
}
});
}
一些缺点(总有一个但是):
- 这是阻塞。所以最好在另一个配置为阻塞 IO 的 Akka thread-pool 上执行它。
- 如前所述,转换为 byte[] 仅适用于小文件,因为它使用内存进行缓冲。如果您只提供小文件(想想网站级别的图像),这应该不是问题。请参阅:http://docs.oracle.com/javase/tutorial/essential/io/file.html 了解使用 NIO2 读取文件的不同方式。
我已经设法用更简单的方法解决了这个问题:
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);
}
}
路线示例:
GET /image/:file controllers.ExternalImagesController.image(file: String)
对于大图像文件,您可以使用流式处理。
官方文档可以在这方面为您提供帮助。
我需要从不在类路径中的绝对路径提供图像文件。当我使用 Assets.at(path, file)
时,它只在 /assets
内搜索。我已将 url 映射到控制器函数上,如下所示:
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);
}
我怎样才能使这个工作?
注意:使用 Assets
提供图像的原因是因为自动标记功能可以轻松发送未修改的 http 304。似乎没有 play 独立于 Assets
Assets.at() 仅适用于添加到类路径 build-time 的资产。 参见:https://www.playframework.com/documentation/2.4.x/Assets
解决方案是从磁盘读取文件作为 byte[],然后 return 响应中的 byte[] body。
将图像转换为字节[](此解决方案仅适用于小文件,对于大文件请查看流):
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();
}
});
}
使用toBytes() 服务图像的控制器:
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());
}
});
}
路线:
POST /img controllers.YourControllerName.img()
如果需要 ETag 支持:
(不添加日期或 Last-Modified headers,因为如果使用 ETag header 则不需要它们):
获取文件的 SHA1:
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;
}
});
}
设置 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;
}
支持 ETag 的控制器:
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());
}
});
}
一些缺点(总有一个但是):
- 这是阻塞。所以最好在另一个配置为阻塞 IO 的 Akka thread-pool 上执行它。
- 如前所述,转换为 byte[] 仅适用于小文件,因为它使用内存进行缓冲。如果您只提供小文件(想想网站级别的图像),这应该不是问题。请参阅:http://docs.oracle.com/javase/tutorial/essential/io/file.html 了解使用 NIO2 读取文件的不同方式。
我已经设法用更简单的方法解决了这个问题:
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);
}
}
路线示例:
GET /image/:file controllers.ExternalImagesController.image(file: String)
对于大图像文件,您可以使用流式处理。 官方文档可以在这方面为您提供帮助。