Maneira recomendada de salvar arquivos carregados em um aplicativo servlet

121

Eu li aqui que não se deve salvar o arquivo no servidor, pois ele não é portátil, transacional e requer parâmetros externos. No entanto, considerando que preciso de uma solução tmp para o tomcat (7) e que tenho controle (relativo) sobre a máquina do servidor, quero saber:

  • Qual é o melhor local para salvar o arquivo? Devo salvá-lo /WEB-INF/uploads(desaconselhado aqui ) ou em algum lugar abaixo $CATALINA_BASE(ver aqui ) ou ...? O tutorial do JavaEE 6 obtém o caminho do usuário (: wtf :). NB: O arquivo não deve ser baixado por qualquer meio.

  • Devo configurar um parâmetro de configuração conforme detalhado aqui ? Eu apreciaria algum código (prefiro dar a ele um caminho relativo - pelo menos é portátil para o Tomcat) - Part.write()parece promissor - mas aparentemente precisa de um caminho absoluto

  • Eu estaria interessado em uma exposição das desvantagens dessa abordagem versus um repositório de banco de dados / JCR

Infelizmente, o FileServlet da @BalusC se concentra no download de arquivos, enquanto sua resposta no upload de arquivos ignora a parte de onde salvar o arquivo.

Uma solução facilmente conversível para usar uma implementação de DB ou JCR (como jackrabbit ) seria preferível.

Mr_and_Mrs_D
fonte
Para minha maneira final de fazê-lo, veja a resposta abaixo
Mr_and_Mrs_D 3/17/17

Respostas:

165

Armazene-o em qualquer lugar em um local acessível, exceto na pasta de projeto do IDE, também conhecida como pasta de implantação do servidor, pelos motivos mencionados na resposta a Imagem carregada disponível apenas após a atualização da página :

  1. As alterações na pasta do projeto do IDE não são refletidas imediatamente na pasta de trabalho do servidor. Há um tipo de trabalho em segundo plano no IDE, que cuida para que a pasta de trabalho do servidor seja sincronizada com as últimas atualizações (isso é, nos termos do IDE, chamado "publicação"). Essa é a principal causa do problema que você está vendo.

  2. No código do mundo real, existem circunstâncias em que o armazenamento de arquivos carregados na pasta de implantação do aplicativo da web não funcionará. Alguns servidores (por padrão ou por configuração) não expandem o arquivo WAR implementado no sistema de arquivos do disco local, mas totalmente na memória. Você não pode criar novos arquivos na memória sem basicamente editar o arquivo WAR implementado e reimplementá-lo.

  3. Mesmo quando o servidor expande o arquivo WAR implantado no sistema de arquivos do disco local, todos os arquivos recém-criados serão perdidos em uma reimplementação ou até em uma simples reinicialização, simplesmente porque esses novos arquivos não fazem parte do arquivo WAR original.

Realmente não importa para mim ou qualquer outra pessoa onde exatamente no sistema de arquivos do disco local ele será salvo, contanto que você não não nunca usar getRealPath()método . Usando esse método é em qualquer caso alarmante.

O caminho para o local de armazenamento pode, por sua vez, ser definido de várias maneiras. Você tem que fazer tudo sozinho . Talvez seja aí que sua confusão é causada, porque de alguma forma você esperava que o servidor fizesse isso automaticamente. Por favor note que @MultipartConfig(location)se não especificar o destino de upload final, mas o local de armazenamento temporário para o tamanho do arquivo do caso exceder o limite de armazenamento de memória.

Portanto, o caminho para o local de armazenamento final pode ser definido de uma das seguintes maneiras:

  • Codificado:

      File uploads = new File("/path/to/uploads");
  • Variável de ambiente via SET UPLOAD_LOCATION=/path/to/uploads:

      File uploads = new File(System.getenv("UPLOAD_LOCATION"));
  • Argumento da VM durante a inicialização do servidor via -Dupload.location="/path/to/uploads":

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

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

      File uploads = new File(getServletContext().getInitParameter("upload.location"));
  • Se houver, use o local fornecido pelo servidor, por exemplo, no JBoss AS / WildFly :

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

De qualquer forma, você pode facilmente referenciar e salvar o arquivo da seguinte maneira:

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

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

Ou, quando você deseja gerar automaticamente um nome de arquivo exclusivo para impedir que os usuários sobrescrevam arquivos existentes com coincidentemente o mesmo nome:

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

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

Como obter partem JSP / Servlet é respondido em Como fazer upload de arquivos para o servidor usando JSP / Servlet? e como obter partno JSF é respondido em Como fazer upload de arquivo usando o JSF 2.2 <h: inputFile>? Onde está o arquivo salvo?

Nota: não usePart#write() , pois interpreta o caminho em relação ao local de armazenamento temporário definido em@MultipartConfig(location) .

Veja também:

BalusC
fonte
Os @MultipartConfig(location)especifica o temporária localização storge qual o servidor deve usar quando o tamanho do arquivo exceder o limite de armazenamento de memória, e não o local de armazenamento permanente, onde você iria em última análise, como se para ser armazenado. O valor padrão é o caminho, conforme identificado pela java.io.tmpdirpropriedade do sistema. Consulte também esta resposta relacionada a uma tentativa falha do JSF: stackoverflow.com/questions/18478154/…
BalusC
1
Obrigado - espero não parecer idiota, mas esta citação de Part.write>> Isso permite que uma implementação específica use, por exemplo, renomear arquivos, sempre que possível, em vez de copiar todos os dados subjacentes, obtendo assim um benefício significativo de desempenho em conjunto com alguns método de dizer alguma apache lib desconhecido "cut" (vs cópia) me pouparia o trabalho de escrever os bytes mim mesmo - e recriando um arquivo já existe (ver também aqui )
Mr_and_Mrs_D
Sim, se você já estiver no Servlet 3.0, poderá usá-lo Part#write(). Eu atualizei a resposta com ele.
precisa saber é o seguinte
Muito obrigado por manter a postagem atualizada - existe uma propriedade para o Tomcat como "jboss.server.data.dir"?
Mr_and_Mrs_D
1
Não, não tem.
BalusC
7

Postei minha maneira final de fazê-lo com base na resposta aceita:

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

Onde :

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

e o /WEB-INF/app.properties:

upload.location=C:/_/

HTH e se você encontrar um bug, me avise

Mr_and_Mrs_D
fonte
1
E se eu quiser uma solução independente de SO, que funcione nos dois casos (win / ux)? Preciso definir um caminho diferente para upload.location ou há outra dica?
Pikimota