124

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.

Community
  • 1
  • 1
Mr_and_Mrs_D
  • 27,070
  • 30
  • 156
  • 325

2 Answers2

167

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:

BalusC
  • 992,635
  • 352
  • 3,478
  • 3,452
  • 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: http://stackoverflow.com/questions/18478154/write-file-into-disk-using-jsf-2-2-inputfile/18533832#18533832 – BalusC Sep 06 '13 at 19:04
  • 1
    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_](http://docs.oracle.com/javaee/6/api/javax/servlet/http/Part.html#write%28java.lang.String%29) 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](http://stackoverflow.com/q/11615832/281545)) – Mr_and_Mrs_D Sep 06 '13 at 19:14
  • Yes, if you're already on Servlet 3.0, you could make use of `Part#write()`. I updated the answer with it. – BalusC Sep 06 '13 at 19:16
  • Thank you very much for keeping the post updated - is there such a property for Tomcat as `"jboss.server.data.dir"` ? – Mr_and_Mrs_D Sep 06 '13 at 21:20
  • 1
    Nope, it doesn't have. – BalusC Sep 06 '13 at 21:33
  • 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 unclear – Mr_and_Mrs_D Sep 07 '13 at 11:24
  • http://stackoverflow.com/questions/2161054/where-to-place-configuration-properties-files-in-a-jsp-servlet-web-application/2161583#2161583 – BalusC Sep 07 '13 at 12:54
  • glassfish does not support absolute file paths in `Part.write()` apparently : see [here](http://stackoverflow.com/questions/19738244/glassfish-part-write-no-absolute-paths-workaround) – Mr_and_Mrs_D Nov 02 '13 at 02:11
  • 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. – Marcel Korpel Aug 09 '15 at 19:34
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 : http://stackoverflow.com/a/2424824/281545
        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

Mr_and_Mrs_D
  • 27,070
  • 30
  • 156
  • 325
  • 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? – pikimota Jul 09 '18 at 15:41