Recommended way to save uploaded files in a servlet application
Asked Answered
N

3

127

I read here that one should not save the file in the server anyway as it is not portable, transactional and requires external parameters. However, given that I need a tmp solution for tomcat (7) and that I have (relative) control over the server machine I want to know :

  • What is the best place to save the file ? Should I save it in /WEB-INF/uploads (advised against here) or someplace under $CATALINA_BASE (see here) or ... ? The JavaEE 6 tutorial gets the path from the user (:wtf:). NB : The file should not be downloadable by any means.

  • Should I set up a config parameter as detailed here ? I'd appreciate some code (I'd rather give it a relative path - so it is at least Tomcat portable) - Part.write() looks promising - but apparently needs a absolute path

  • I'd be interested in an exposition of the disadvantages of this approach vs a database/JCR repository one

Unfortunately the FileServlet by @BalusC concentrates on downloading files, while his answer on uploading files skips the part on where to save the file.

A solution easily convertible to use a DB or a JCR implementation (like jackrabbit) would be preferable.

Nourishment answered 6/9, 2013 at 18:49 Comment(1)
For my final way of doing it see answer belowNourishment
H
171

Store it anywhere in an accessible location except of the IDE's project folder aka the server's deploy folder, for reasons mentioned in the answer to Uploaded image only available after refreshing the page:

  1. Changes in the IDE's project folder does not immediately get reflected in the server's work folder. There's kind of a background job in the IDE which takes care that the server's work folder get synced with last updates (this is in IDE terms called "publishing"). This is the main cause of the problem you're seeing.

  2. In real world code there are circumstances where storing uploaded files in the webapp's deploy folder will not work at all. Some servers do (either by default or by configuration) not expand the deployed WAR file into the local disk file system, but instead fully in the memory. You can't create new files in the memory without basically editing the deployed WAR file and redeploying it.

  3. Even when the server expands the deployed WAR file into the local disk file system, all newly created files will get lost on a redeploy or even a simple restart, simply because those new files are not part of the original WAR file.

It really doesn't matter to me or anyone else where exactly on the local disk file system it will be saved, as long as you do not ever use getRealPath() method. Using that method is in any case alarming.

The path to the storage location can in turn be definied in many ways. You have to do it all by yourself. Perhaps this is where your confusion is caused because you somehow expected that the server does that all automagically. Please note that @MultipartConfig(location) does not specify the final upload destination, but the temporary storage location for the case file size exceeds memory storage threshold.

So, the path to the final storage location can be definied in either of the following ways:

  • Hardcoded:

      File uploads = new File("/path/to/uploads");
    
  • Environment variable via SET UPLOAD_LOCATION=/path/to/uploads:

      File uploads = new File(System.getenv("UPLOAD_LOCATION"));
    
  • VM argument during server startup via -Dupload.location="/path/to/uploads":

      File uploads = new File(System.getProperty("upload.location"));
    
  • *.properties file entry as upload.location=/path/to/uploads:

      File uploads = new File(properties.getProperty("upload.location"));
    
  • web.xml <context-param> with name upload.location and value /path/to/uploads:

      File uploads = new File(getServletContext().getInitParameter("upload.location"));
    
  • If any, use the server-provided location, e.g. in JBoss AS/WildFly:

      File uploads = new File(System.getProperty("jboss.server.data.dir"), "uploads");
    

Either way, you can easily reference and save the file as follows:

File file = new File(uploads, "somefilename.ext");

try (InputStream input = part.getInputStream()) {
    Files.copy(input, file.toPath());
}

Or, when you want to autogenerate an unique file name to prevent users from overwriting existing files with coincidentally the same name:

File file = File.createTempFile("somefilename-", ".ext", uploads);

try (InputStream input = part.getInputStream()) {
    Files.copy(input, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}

How to obtain part in JSP/Servlet is answered in How to upload files to server using JSP/Servlet? and how to obtain part in JSF is answered in How to upload file using JSF 2.2 <h:inputFile>? Where is the saved File?

Note: do not use Part#write() as it interprets the path relative to the temporary storage location defined in @MultipartConfig(location). Also make absolutely sure that you aren't corrupting binary files such as PDF files or image files by converting bytes to characters during reading/writing by incorrectly using a Reader/Writer instead of InputStream/OutputStream.

See also:

Holocene answered 6/9, 2013 at 18:58 Comment(9)
The @MultipartConfig(location) specifies the temporary storge location which the server should use when the file size exceeds the threshold for memory storage, not the permanent storage location where you'd ultimately like it to be stored. This value defaults to the path as identified by java.io.tmpdir system property. See also this related answer to a failed JSF attempt: stackoverflow.com/questions/18478154/…Holocene
Thanks - hope I do not sound idiot but this quote from Part.write >> This allows a particular implementation to use, for example, file renaming, where possible, rather than copying all of the underlying data, thus gaining a significant performance benefit in conjunction with some unknown "cut" (vs copy) method from say some apache lib would save me the hassle of writing the bytes myself - and recreating a file already there (see also here)Nourishment
Yes, if you're already on Servlet 3.0, you could make use of Part#write(). I updated the answer with it.Holocene
Thank you very much for keeping the post updated - is there such a property for Tomcat as "jboss.server.data.dir" ?Nourishment
Nope, it doesn't have.Holocene
Feel free to edit my updated answer (I posted my solution) with your insights :) Especially the // "/WEB-INF/app.properties" also works... is the only part that remains somewhat unclearNourishment
stackoverflow.com/questions/2161054/…Holocene
glassfish does not support absolute file paths in Part.write() apparently : see hereNourishment
Note that when you change the new File part to File.createTempFile(…) you should also use the StandardCopyOption.REPLACE_EXISTING flag to Files.copy, otherwise you'll get an error that the file already exists.Aleppo
N
7

I post my final way of doing it based on the accepted answer:

@SuppressWarnings("serial")
@WebServlet("/")
@MultipartConfig
public final class DataCollectionServlet extends Controller {

    private static final String UPLOAD_LOCATION_PROPERTY_KEY="upload.location";
    private String uploadsDirName;

    @Override
    public void init() throws ServletException {
        super.init();
        uploadsDirName = property(UPLOAD_LOCATION_PROPERTY_KEY);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // ...
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        Collection<Part> parts = req.getParts();
        for (Part part : parts) {
            File save = new File(uploadsDirName, getFilename(part) + "_"
                + System.currentTimeMillis());
            final String absolutePath = save.getAbsolutePath();
            log.debug(absolutePath);
            part.write(absolutePath);
            sc.getRequestDispatcher(DATA_COLLECTION_JSP).forward(req, resp);
        }
    }

    // helpers
    private static String getFilename(Part part) {
        // courtesy of BalusC : https://mcmap.net/q/16755/-how-can-i-upload-files-to-a-server-using-jsp-servlet
        for (String cd : part.getHeader("content-disposition").split(";")) {
            if (cd.trim().startsWith("filename")) {
                String filename = cd.substring(cd.indexOf('=') + 1).trim()
                        .replace("\"", "");
                return filename.substring(filename.lastIndexOf('/') + 1)
                        .substring(filename.lastIndexOf('\\') + 1); // MSIE fix.
            }
        }
        return null;
    }
}

where :

@SuppressWarnings("serial")
class Controller extends HttpServlet {

    static final String DATA_COLLECTION_JSP="/WEB-INF/jsp/data_collection.jsp";
    static ServletContext sc;
    Logger log;
    // private
    // "/WEB-INF/app.properties" also works...
    private static final String PROPERTIES_PATH = "WEB-INF/app.properties";
    private Properties properties;

    @Override
    public void init() throws ServletException {
        super.init();
        // synchronize !
        if (sc == null) sc = getServletContext();
        log = LoggerFactory.getLogger(this.getClass());
        try {
            loadProperties();
        } catch (IOException e) {
            throw new RuntimeException("Can't load properties file", e);
        }
    }

    private void loadProperties() throws IOException {
        try(InputStream is= sc.getResourceAsStream(PROPERTIES_PATH)) {
                if (is == null)
                    throw new RuntimeException("Can't locate properties file");
                properties = new Properties();
                properties.load(is);
        }
    }

    String property(final String key) {
        return properties.getProperty(key);
    }
}

and the /WEB-INF/app.properties :

upload.location=C:/_/

HTH and if you find a bug let me know

Nourishment answered 6/9, 2013 at 18:49 Comment(1)
What if I want a SO independent solution, that works in both (win/ux) case? Do I have to set different upload.location path or there is some other hint?Thorley
U
0

The Servlet Specification provides a place for each deployed web application to use as temporary physical file storage. You should use it for that purpose. For example, when using multipart-form-upload:

Part part = request.getPart("param-name");
File tmpDir = (File)getServletContext().getAttribute("javax.servlet.context.tempdir");
File targetFile = new File(tmpDir, "tmp-file-name");
part.write(targetFile.getAbsolutePath());

Now, you can do whatever you want with targetFile.

Upholster answered 13/11, 2023 at 16:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.