Рекомендуемый способ сохранения загруженных файлов в приложении сервлета

121

Я читал здесь, что в любом случае не следует сохранять файл на сервере, поскольку он не переносится, не требует транзакций и требует внешних параметров. Однако, учитывая, что мне нужно решение tmp для tomcat (7) и что у меня есть (относительный) контроль над серверной машиной, я хочу знать:

  • Как лучше всего сохранить файл? Следует ли мне сохранить его /WEB-INF/uploads( здесь не рекомендуется ) или где-то ниже $CATALINA_BASE(см. Здесь ) или ...? Учебник по JavaEE 6 получает путь от пользователя (: wtf :). NB: файл ни в коем случае нельзя скачивать.

  • Следует ли мне настроить параметр конфигурации, как описано здесь ? Я был бы признателен за некоторый код (я бы предпочел дать ему относительный путь - так что это, по крайней мере, переносимый Tomcat) - Part.write()выглядит многообещающе - но, очевидно, нужен абсолютный путь

  • Мне было бы интересно рассказать о недостатках этого подхода по сравнению с репозиторием базы данных / JCR.

К сожалению, FileServlet по @BalusC концентрируется на загрузку файлов, а его ответ на загрузки файлов пропускает часть о том, куда сохранить файл.

Было бы предпочтительнее решение, легко конвертируемое для использования БД или реализации JCR (например, jackrabbit ).

Mr_and_Mrs_D
источник
Мой последний способ сделать это см. В ответе ниже
Mr_and_Mrs_D

Ответы:

165

Храните его в любом доступном месте, кроме папки проекта IDE, также известной как папка развертывания сервера, по причинам, указанным в ответе на « Загруженное изображение», доступном только после обновления страницы :

  1. Изменения в папке проекта IDE не сразу отражаются в рабочей папке сервера. В среде IDE есть своего рода фоновое задание, которое заботится о том, чтобы рабочая папка сервера синхронизировалась с последними обновлениями (в терминах IDE это называется «публикация»). Это основная причина проблемы, которую вы видите.

  2. В реальном коде бывают ситуации, когда сохранение загруженных файлов в папке развертывания веб-приложения вообще не работает. Некоторые серверы (либо по умолчанию, либо по конфигурации) не разворачивают развернутый файл WAR в файловую систему локального диска, а полностью в памяти. Вы не можете создавать новые файлы в памяти, не отредактировав развернутый файл WAR и не повторно развернув его.

  3. Даже когда сервер разворачивает развернутый файл WAR в файловую систему локального диска, все вновь созданные файлы будут потеряны при повторном развертывании или даже при простом перезапуске просто потому, что эти новые файлы не являются частью исходного файла WAR.

Для меня или кого-либо еще не имеет значения, где именно в файловой системе локального диска он будет сохранен, если вы никогда не используете getRealPath()метод . В любом случае использование этого метода вызывает тревогу.

Путь к месту хранения, в свою очередь, можно определить разными способами. Вы должны сделать все это сами . Возможно, именно здесь ваше замешательство вызвано тем, что вы каким-то образом ожидали, что сервер сделает все это автоматически. Обратите внимание , что @MultipartConfig(location)это не указать конечный пункт назначения загрузки, но место временного хранения размера случае файла превышает порог хранения памяти.

Итак, путь к окончательному хранилищу можно определить одним из следующих способов:

  • HARDCODED:

      File uploads = new File("/path/to/uploads");
  • Переменная среды через SET UPLOAD_LOCATION=/path/to/uploads:

      File uploads = new File(System.getenv("UPLOAD_LOCATION"));
  • Аргумент виртуальной машины во время запуска сервера через -Dupload.location="/path/to/uploads":

      File uploads = new File(System.getProperty("upload.location"));
  • *.propertiesзапись файла как upload.location=/path/to/uploads:

      File uploads = new File(properties.getProperty("upload.location"));
  • web.xml <context-param>с именем upload.locationи значением /path/to/uploads:

      File uploads = new File(getServletContext().getInitParameter("upload.location"));
  • Если есть, используйте расположение, указанное сервером, например, в JBoss AS / WildFly :

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

В любом случае вы можете легко сослаться на файл и сохранить его следующим образом:

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

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

Или, если вы хотите автоматически сгенерировать уникальное имя файла, чтобы пользователи не перезаписывали существующие файлы с одинаковым именем:

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

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

Как получить partв JSP / Servlet, ответ на вопрос Как загрузить файлы на сервер с помощью JSP / Servlet? и как получить partв JSF, ответ в Как загрузить файл с помощью JSF 2.2 <h: inputFile>? Где сохраненный файл?

Примечание: не используйте, Part#write()поскольку он интерпретирует путь относительно места временного хранения, определенного в @MultipartConfig(location).

Смотрите также:

BalusC
источник
В @MultipartConfig(location)Определяет временное расположение сторга , который сервер должен использовать , если размер файла превышает порог для хранения памяти, а не постоянное место хранения , где вы бы в конечном счете , как он будет храниться. По умолчанию это значение соответствует пути, указанному в java.io.tmpdirсистемном свойстве. См. Также соответствующий ответ на неудачную попытку JSF: stackoverflow.com/questions/18478154/…
BalusC
1
Спасибо - надеюсь, я не звучу идиотом, но эта цитата из Part.write>> Это позволяет конкретной реализации использовать, например, переименование файлов, где это возможно, вместо копирования всех базовых данных, таким образом получая значительное преимущество в производительности в сочетании с некоторыми неизвестный метод "вырезания" (против копирования), скажем, какой-то apache lib избавит меня от необходимости писать байты самому - и воссоздавать уже существующий файл (см. также здесь )
Mr_and_Mrs_D 06
Да, если вы уже используете Servlet 3.0, вы можете использовать Part#write(). Я обновил ответ этим.
BalusC 06
Большое спасибо за то, что обновляете пост - есть ли такое свойство для Tomcat как "jboss.server.data.dir"?
Mr_and_Mrs_D 06
1
Нет, это не так.
BalusC
7

Я публикую свой последний способ сделать это на основе принятого ответа:

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

где :

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

и /WEB-INF/app.properties:

upload.location=C:/_/

HTH, и если вы обнаружите ошибку, дайте мне знать

Mr_and_Mrs_D
источник
1
Что, если мне нужно SO-независимое решение, которое работает в обоих случаях (win / ux)? Нужно ли мне указывать другой путь upload.location или есть другой совет?
pikimota 09