Forma recomendada de guardar archivos cargados en una aplicación de servlet

121

Leí aquí que no se debe guardar el archivo en el servidor de todos modos, ya que no es portátil, transaccional y requiere parámetros externos. Sin embargo, dado que necesito una solución tmp para tomcat (7) y que tengo control (relativo) sobre la máquina del servidor que quiero saber:

  • ¿Cuál es el mejor lugar para guardar el archivo? ¿Debo guardarlo en /WEB-INF/uploads(desaconsejado aquí ) o en algún lugar debajo $CATALINA_BASE(ver aquí ) o ...? El tutorial de JavaEE 6 obtiene la ruta del usuario (: wtf :). NB: el archivo no debe ser descargable de ninguna manera.

  • ¿Debo configurar un parámetro de configuración como se detalla aquí ? Apreciaría algo de código (prefiero darle una ruta relativa, por lo que es al menos portátil Tomcat), Part.write()parece prometedor, pero aparentemente necesita una ruta absoluta

  • Me interesaría una exposición de las desventajas de este enfoque frente a una base de datos / repositorio JCR

Por desgracia, la FileServlet por @BalusC se concentra en la descarga de archivos, mientras que su respuesta en la carga de archivos se salta la parte de dónde guardar el archivo.

Sería preferible una solución fácilmente convertible para usar una implementación DB o JCR (como jackrabbit ).

Mr_and_Mrs_D
fuente
Para mi última forma de hacerlo, vea la respuesta a continuación
Mr_and_Mrs_D

Respuestas:

165

Almacénelo en cualquier lugar en una ubicación accesible, excepto la carpeta de proyectos del IDE, también conocida como la carpeta de implementación del servidor, por las razones mencionadas en la respuesta a la imagen cargada solo disponible después de actualizar la página :

  1. Los cambios en la carpeta de proyectos del IDE no se reflejan inmediatamente en la carpeta de trabajo del servidor. Hay una especie de trabajo en segundo plano en el IDE que se encarga de que la carpeta de trabajo del servidor se sincronice con las últimas actualizaciones (esto está en términos del IDE llamado "publicación"). Esta es la causa principal del problema que estás viendo.

  2. En el código del mundo real, hay circunstancias en las que el almacenamiento de archivos cargados en la carpeta de implementación de la aplicación web no funcionará en absoluto. Algunos servidores (ya sea de forma predeterminada o por configuración) no expanden el archivo WAR implementado en el sistema de archivos del disco local, sino que lo hacen completamente en la memoria. No puede crear archivos nuevos en la memoria sin editar básicamente el archivo WAR desplegado y volver a implementarlo.

  3. Incluso cuando el servidor expande el archivo WAR desplegado en el sistema de archivos del disco local, todos los archivos recién creados se perderán en una nueva implementación o incluso en un simple reinicio, simplemente porque esos nuevos archivos no forman parte del archivo WAR original.

Realmente no importa a mí ni a nadie dónde exactamente en el sistema de archivos del disco local que se guardará, siempre y cuando no no siempre utiliza getRealPath()el método . Usar ese método es en cualquier caso alarmante.

La ruta a la ubicación de almacenamiento puede definirse a su vez de muchas maneras. Tienes que hacerlo todo por ti mismo . Quizás es aquí donde se produce su confusión porque de alguna manera esperaba que el servidor lo hiciera todo de forma automática. Tenga en cuenta que @MultipartConfig(location)no no especifica el destino final de subida, pero la ubicación de almacenamiento temporal para el tamaño de archivo del caso supera el umbral de almacenamiento de memoria.

Por lo tanto, la ruta a la ubicación de almacenamiento final se puede definir de cualquiera de las siguientes maneras:

  • Hardcoded:

      File uploads = new File("/path/to/uploads");
  • Variable de entorno a través de SET UPLOAD_LOCATION=/path/to/uploads:

      File uploads = new File(System.getenv("UPLOAD_LOCATION"));
  • Argumento de VM durante el inicio del servidor a través de -Dupload.location="/path/to/uploads":

      File uploads = new File(System.getProperty("upload.location"));
  • *.propertiesentrada de archivo como upload.location=/path/to/uploads:

      File uploads = new File(properties.getProperty("upload.location"));
  • web.xml <context-param>con nombre upload.locationy valor /path/to/uploads:

      File uploads = new File(getServletContext().getInitParameter("upload.location"));
  • Si corresponde, use la ubicación proporcionada por el servidor, por ejemplo, en JBoss AS / WildFly :

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

De cualquier manera, puede hacer referencia y guardar fácilmente el archivo de la siguiente manera:

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

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

O, cuando desee generar automáticamente un nombre de archivo único para evitar que los usuarios sobrescriban los archivos existentes con el mismo nombre:

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

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

¿Cómo obtener parten JSP / Servlet se responde en Cómo cargar archivos al servidor utilizando JSP / Servlet? y cómo obtener parten JSF se responde en Cómo cargar un archivo usando JSF 2.2 <h: inputFile>? ¿Dónde está el archivo guardado?

Nota: no utilice, Part#write()ya que interpreta la ruta relativa a la ubicación de almacenamiento temporal definida en @MultipartConfig(location).

Ver también:

BalusC
fuente
Los @MultipartConfig(location)Especifica el temporal ubicación storgé el que el servidor debe utilizar cuando el tamaño del archivo supera el umbral de almacenamiento de memoria, no la ubicación de almacenamiento permanente en los que desea en última instancia, al igual que para ser almacenados. Este valor predeterminado es la ruta identificada por la java.io.tmpdirpropiedad del sistema. Vea también esta respuesta relacionada a un intento fallido de JSF: stackoverflow.com/questions/18478154/…
BalusC
1
Gracias, espero que no parezca idiota, pero esta cita de Part.write>> Esto permite que una implementación particular use, por ejemplo, el cambio de nombre de archivos, cuando sea posible, en lugar de copiar todos los datos subyacentes, obteniendo así un beneficio de rendimiento significativo junto con algunos método de decir que algunos Apache lib desconocido "corte" (vs copia) me ahorraría la molestia de escribir los bytes a mí mismo - y volver a crear un archivo que ya existe (ver también aquí )
Mr_and_Mrs_D
Sí, si ya está en Servlet 3.0, puede utilizarlo Part#write(). Actualicé la respuesta con ella.
BalusC
Muchas gracias por mantener la publicación actualizada. ¿Existe tal propiedad para Tomcat como "jboss.server.data.dir"?
Mr_and_Mrs_D
1
No, no tiene.
BalusC
7

Publico mi forma final de hacerlo en función de la respuesta aceptada:

@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;
    }
}

dónde :

@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);
    }
}

y las /WEB-INF/app.properties:

upload.location=C:/_/

HTH y si encuentras un error avísame

Mr_and_Mrs_D
fuente
1
¿Qué sucede si quiero una solución SO independiente que funcione en ambos casos (win / ux)? ¿Tengo que establecer una ruta de subida y ubicación diferente o hay alguna otra pista?
pikimota