Simplest way to serve static data from outside the application server in a Java web application
Asked Answered
V

11

141

I have a Java web application running on Tomcat. I want to load static images that will be shown both on the Web UI and in PDF files generated by the application. Also new images will be added and saved by uploading via the Web UI.

It's not a problem to do this by having the static data stored within the web container but storing and loading them from outside the web container is giving me headache.

I'd prefer not to use a separate web server like Apache for serving the static data at this point. I also don't like the idea of storing the images in binary in a database.

I've seen some suggestions like having the image directory being a symbolic link pointing to a directory outside the web container, but will this approach work both on Windows and *nix environments?

Some suggest writing a filter or a servlet for handling the image serving but those suggestions have been very vague and high-level without pointers to more detailed information on how to accomplish this.

Veracruz answered 28/11, 2009 at 10:58 Comment(0)
O
175

I've seen some suggestions like having the image directory being a symbolic link pointing to a directory outside the web container, but will this approach work both on Windows and *nix environments?

If you adhere the *nix filesystem path rules (i.e. you use exclusively forward slashes as in /path/to/files), then it will work on Windows as well without the need to fiddle around with ugly File.separator string-concatenations. It would however only be scanned on the same working disk as from where this command is been invoked. So if Tomcat is for example installed on C: then the /path/to/files would actually point to C:\path\to\files.

If the files are all located outside the webapp, and you want to have Tomcat's DefaultServlet to handle them, then all you basically need to do in Tomcat is to add the following Context element to /conf/server.xml inside <Host> tag:

<Context docBase="/path/to/files" path="/files" />

This way they'll be accessible through http://example.com/files/.... For Tomcat-based servers such as JBoss EAP 6.x or older, the approach is basically the same, see also here. GlassFish/Payara configuration example can be found here and WildFly configuration example can be found here.

If you want to have control over reading/writing files yourself, then you need to create a Servlet for this which basically just gets an InputStream of the file in flavor of for example FileInputStream and writes it to the OutputStream of the HttpServletResponse.

On the response, you should set the Content-Type header so that the client knows which application to associate with the provided file. And, you should set the Content-Length header so that the client can calculate the download progress, otherwise it will be unknown. And, you should set the Content-Disposition header to attachment if you want a Save As dialog, otherwise the client will attempt to display it inline. Finally just write the file content to the response output stream.

Here's a basic example of such a servlet:

@WebServlet("/files/*")
public class FileServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        String filename = URLDecoder.decode(request.getPathInfo().substring(1), "UTF-8");
        File file = new File("/path/to/files", filename);
        response.setHeader("Content-Type", getServletContext().getMimeType(filename));
        response.setHeader("Content-Length", String.valueOf(file.length()));
        response.setHeader("Content-Disposition", "inline; filename=\"" + file.getName() + "\"");
        Files.copy(file.toPath(), response.getOutputStream());
    }

}

When mapped on an url-pattern of for example /files/*, then you can call it by http://example.com/files/image.png. This way you can have more control over the requests than the DefaultServlet does, such as providing a default image (i.e. if (!file.exists()) file = new File("/path/to/files", "404.gif") or so). Also using the request.getPathInfo() is preferred above request.getParameter() because it is more SEO friendly and otherwise IE won't pick the correct filename during Save As.

You can reuse the same logic for serving files from database. Simply replace new FileInputStream() by ResultSet#getInputStream().

See also:

Opportunity answered 28/11, 2009 at 11:57 Comment(13)
@SalutonMondo: the way with least effort.Opportunity
@BalusC, I tried this: <Context docBase="/path/to/images" path="/images" /> in Windows, but getting path relative to the webapps folder: C:\install\apache-tomcat-8.0.26\webapps\tmp] is not validUlphi
On Windows it should be: <Context docBase="C:\tmp\" path="/images" />Ulphi
And getting HTTP Status 404 - /images/ when doing: http://localhost:8080/images/ , BUT a specific file from under tmp works: http://localhost:8080/images/Tulips.jpg is OKUlphi
Don't use it for big files! If you use it for file for example 8 GB and you will start 4x download, your server won't response in future. You should use startAsync and asyncSupported = true.Fusilier
@barwnikk: startAsync won't help here. It has got nothing to do with serving large files. You seem to be totally misunderstanding its purpose. The correct approach to serve large files is shown in the last "See also" link.Opportunity
There are a couple of issues with this answer, although it very succinctly answers the original question. I'm posting this in the hopes that clarification will be added to the answer so readers understand that this isn't "the solution". First, this servlet adds a giant path-traversal bug to any application it's deployed into. That's the most important thing to mention.Starinsky
The second thing is that it doesn't take advantage of a whole bunch of other capabilities that the server and/or HTTP itself can offer. For example, it doesn't handle Range requests, which can significant improve performance when the client only needs a part of the file and not the whole thing. For Tomcat specifically (one of the question's tags), you can hand the file and connection off to the OS and use sendfile to stream directly from the disk cache to the network interface, saving a lot of buffering and trips into and out of the kernel.Starinsky
@Chris: 1) The answer recommends to use Tomcat's DefaultServlet as 1st solution. 2) Advance to the 4th "See also" link.Opportunity
@Opportunity Yes, I appreciate that it's possible to find a more secure and more feature-full solution from this answer. I just think it's worth putting a code-comment in your example saying something like "TODO: MITIGATE DIRECTORY TRAVERSAL" or something like that. SO is unfortunately a great source of copy/paste solutions in today's software.Starinsky
@Chris: not sure why "directory traversal" should be fixed on this servlet. This example does not represent a default servlet. It's already mapped on a fixed prefix URL pattern. If the enduser attempted to open the servlet by e.g. /files/../foo.ext, then the servlet is already not hit in first place, but instead the one behind /foo.ext is hit which is then indeed most probably the server's own default servlet if no mapped servlet exist.Opportunity
@Opportunity Servlet containers are not required by the servlet specification to remove dot-segments from URLs. It's entirely possible that a request to /files/../foo.ext would be delivered to the servlet with <url-mapping>/files/*</url-mapping>. Defense-in-depth is a valuable thing, and it's best to teach people that lesson at every opportunity.Starinsky
@Chris: That's only applicable if you're developing a default servlet.Opportunity
B
9

You can do it by putting your images on a fixed path (for example: /var/images, or c:\images), add a setting in your application settings (represented in my example by the Settings.class), and load them like that, in a HttpServlet of yours:

String filename = Settings.getValue("images.path") + request.getParameter("imageName")
FileInputStream fis = new FileInputStream(filename);

int b = 0;
while ((b = fis.read()) != -1) {
        response.getOutputStream().write(b);
}

Or if you want to manipulate the image:

String filename = Settings.getValue("images.path") + request.getParameter("imageName")
File imageFile = new File(filename);
BufferedImage image = ImageIO.read(imageFile);
ImageIO.write(image, "image/png", response.getOutputStream());

then the html code would be <img src="imageServlet?imageName=myimage.png" />

Of course you should think of serving different content types - "image/jpeg", for example based on the file extension. Also you should provide some caching.

In addition you could use this servlet for quality rescaling of your images, by providing width and height parameters as arguments, and using image.getScaledInstance(w, h, Image.SCALE_SMOOTH), considering performance, of course.

Banky answered 28/11, 2009 at 11:30 Comment(2)
You really don't need the Java 2D API for this, it would only unnecessarily add more overhead. Just read an InputStream and write to OutputStream.Opportunity
Yup, I started the response with the idea of rescaling and other manipulation, but ended up simplifying it.Banky
M
9

Requirement : Accessing the static Resources (images/videos., etc.,) from outside of WEBROOT directory or from local disk

Step 1 :
Create a folder under webapps of tomcat server., let us say the folder name is myproj

Step 2 :
Under myproj create a WEB-INF folder under this create a simple web.xml

code under web.xml

<web-app>
</web-app>

Directory Structure for the above two steps

c:\programfile\apachesoftwarefoundation\tomcat\...\webapps
                                                            |
                                                            |---myproj
                                                            |   |
                                                            |   |---WEB-INF
                                                                |   |
                                                                    |---web.xml

Step 3:
Now create a xml file with name myproj.xml under the following location

c:\programfile\apachesoftwarefoundation\tomcat\conf\catalina\localhost

CODE in myproj.xml:

<Context path="/myproj/images" docBase="e:/myproj/" crossContext="false" debug="0" reloadable="true" privileged="true" /> 

Step 4:
4 A) Now create a folder with name myproj in E drive of your hard disk and create a new

folder with name images and place some images in images folder (e:myproj\images\)

Let us suppose myfoto.jpg is placed under e:\myproj\images\myfoto.jpg

4 B) Now create a folder with name WEB-INF in e:\myproj\WEB-INF and create a web.xml in WEB-INF folder

Code in web.xml

<web-app>
</web-app>

Step 5:
Now create a .html document with name index.html and place under e:\myproj

CODE under index.html Welcome to Myproj

The Directory Structure for the above Step 4 and Step 5 is as follows

E:\myproj
    |--index.html
    |
    |--images
    |     |----myfoto.jpg
    |
    |--WEB-INF
    |     |--web.xml

Step 6:
Now start the apache tomcat server

Step 7:
open the browser and type the url as follows

http://localhost:8080/myproj    

then u display the content which is provided in index.html

Step 8:
To Access the Images under your local hard disk (outside of webroot)

http://localhost:8080/myproj/images/myfoto.jpg
Mebane answered 16/3, 2013 at 4:46 Comment(3)
can you please suggest me, how to do the same thing for dynamic values. I mean I want write the data(xml) into my local directory or and and to read that in my jsp page. Is there any way to write into server managed to directory so that I access them using above procedure??Pennell
although I can run index.html file properly but not images are displaying in web browserHeadpin
My mistake post is working fine I just forgot to put / at the end of E:/myproj I change this to E:/myproj/ and its working fine Thanks @MebaneHeadpin
O
8

Add to server.xml :

 <Context docBase="c:/dirtoshare" path="/dir" />

Enable dir file listing parameter in web.xml :

    <init-param>
        <param-name>listings</param-name>
        <param-value>true</param-value>
    </init-param>
Orectic answered 12/5, 2015 at 10:27 Comment(3)
With the change in web.xml I can get a list of files in order to send it to select box ?Gaynell
this change will be in tomcat's web.xml, not your applicationUnmade
Thanks for this! Is enabling dir file listing parameter in web.xml necessary?Alack
L
7

This is story from my workplace:
- We try to upload multiply images and document files use Struts 1 and Tomcat 7.x.
- We try to write uploaded files to file system, filename and full path to database records.
- We try to separate file folders outside web app directory. (*)

The below solution is pretty simple, effective for requirement (*):

In file META-INF/context.xml file with the following content: (Example, my application run at http://localhost:8080/ABC, my application / project named ABC). (this is also full content of file context.xml)

<?xml version="1.0" encoding="UTF-8"?>
<Context path="/ABC" aliases="/images=D:\images,/docs=D:\docs"/>

(works with Tomcat version 7 or later)

Result: We have been created 2 alias. For example, we save images at: D:\images\foo.jpg and view from link or using image tag:

<img src="http://localhost:8080/ABC/images/foo.jsp" alt="Foo" height="142" width="142">

or

<img src="/images/foo.jsp" alt="Foo" height="142" width="142">

(I use Netbeans 7.x, Netbeans seem auto create file WEB-INF\context.xml)

Lysin answered 2/6, 2015 at 16:1 Comment(0)
P
2

If you decide to dispatch to FileServlet then you will also need allowLinking="true" in context.xml in order to allow FileServlet to traverse the symlinks.

See http://tomcat.apache.org/tomcat-6.0-doc/config/context.html

Punctuation answered 25/1, 2011 at 17:3 Comment(0)
B
1

If you want to work with JAX-RS (e.g. RESTEasy) try this:

@Path("/pic")
public Response get(@QueryParam("url") final String url) {
    String picUrl = URLDecoder.decode(url, "UTF-8");

    return Response.ok(sendPicAsStream(picUrl))
            .header(HttpHeaders.CONTENT_TYPE, "image/jpg")
            .build();
}

private StreamingOutput sendPicAsStream(String picUrl) {
    return output -> {
        try (InputStream is = (new URL(picUrl)).openStream()) {
            ByteStreams.copy(is, output);
        }
    };
}

using javax.ws.rs.core.Response and com.google.common.io.ByteStreams

Barefaced answered 3/4, 2018 at 1:4 Comment(0)
E
0

if anyone not able to resolve his problem with accepted answer, then note these below considerations:

  1. no need to mention localhost:<port> with <img> src attribute.
  2. make sure you are running this project outside eclipse, because eclipse creates context docBase entry on its own inside its local server.xml file.
Environ answered 26/10, 2017 at 8:37 Comment(0)
A
0

Read the InputStream of a file and write it to ServletOutputStream for sending binary data to the client.

  • Local file You can read a file directly using FileInputStream('path/image.png').
  • Mongo DataBase file's you can get InputStream using GridFS.
@WebServlet("/files/URLStream")
public class URLStream extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public URLStream() {
        super();
    }

    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        File source = new File("D:\\SVN_Commit.PNG");
        long start = System.nanoTime();

        InputStream image = new FileInputStream(source);

        /*String fileID = request.getParameter("id");
        System.out.println("Requested File ID : "+fileID);
        // Mongo DB GridFS - https://mcmap.net/q/17061/-mongodb-with-csv-data
        image = outputImageFile.getInputStream();*/

        if( image != null ) {
            BufferedInputStream bin = null;
            BufferedOutputStream bout = null;
            ServletOutputStream sos = response.getOutputStream();
            try {
                bin = new BufferedInputStream( image );
                bout = new BufferedOutputStream( sos );
                int ch =0; ;
                while((ch=bin.read())!=-1) {
                    bout.write(ch);
                }
            } finally {
                bin.close();
                image.close();
                bout.close();
                sos.close();
            }

        } else {
            PrintWriter writer = response.getWriter();
            writer.append("Something went wrong with your request.");
            System.out.println("Image not available.");
        }
        System.out.println("Time taken by Stream Copy = "+(System.nanoTime()-start));
    }
}

Result the URL directly to the src attibute.

<img src='http://172.0.0.1:8080/ServletApp/files/URLStream?id=5a575be200c117cc2500003b' alt="mongodb File"/>
<img src='http://172.0.0.1:8080/ServletApp/files/URLStream' alt="local file"/>

<video controls="controls" src="http://172.0.0.1:8080/ServletApp/files/URLStream"></video>
Abortifacient answered 12/1, 2018 at 11:13 Comment(0)
S
0

You can edit 2 files in the conf directory:
First, edit server.xml file:
You will see <Engine></Engine> has many <Host></Host> tag.
In the <Host> with name is: "localhost", change <Context> to:

<Host>
    <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
    ...
    <Context docBase="/home/your_directory_contain_image/" reloadable="true" path="images"></Context>
</Host>

with:
docBase is the path to the directory containing your images.
path: is the path that will add to url when showing the image.
Your target will like: http://localhost:8080/images/image1.png

Next, edit web.xml file, in the <servlet> with name: <servlet-name>default</servlet-name> edit like:

    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

It will start like an image server when you start Tomcat
Hope that it helps

Shetrit answered 22/3, 2023 at 9:51 Comment(0)
B
-1

I did it even simpler. Problem: A CSS file had url links to img folder. Gets 404.

I looked at url, http://tomcatfolder:port/img/blablah.png, which does not exist. But, that is really pointing to the ROOT app in Tomcat.

So I just copied the img folder from my webapp into that ROOT app. Works!

Not recommended for production, of course, but this is for an internal tool dev app.

Bryanbryana answered 31/8, 2016 at 14:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.