Zalecany sposób zapisywania przesłanych plików w aplikacji serwletowej

121

Przeczytałem tutaj, że i tak nie należy zapisywać pliku na serwerze, gdyż nie jest on przenośny, transakcyjny i wymaga parametrów zewnętrznych. Biorąc jednak pod uwagę, że potrzebuję rozwiązania tmp dla tomcat (7) i mam (względną) kontrolę nad maszyną serwera, o której chcę wiedzieć:

  • Jakie jest najlepsze miejsce do zapisania pliku? Powinienem go zapisać w /WEB-INF/uploads(odradzane tutaj ) lub gdzieś pod $CATALINA_BASE(patrz tutaj ) czy ...? Samouczek JavaEE 6 pobiera ścieżkę od użytkownika (: wtf :). Uwaga: plik nie powinien być w żaden sposób możliwy do pobrania.

  • Czy powinienem ustawić parametr konfiguracyjny zgodnie z opisem tutaj ? Byłbym wdzięczny za trochę kodu (wolałbym podać mu ścieżkę względną - więc jest to przynajmniej Tomcat przenośny) - Part.write()wygląda obiecująco - ale najwyraźniej potrzebuje bezwzględnej ścieżki

  • Byłbym zainteresowany prezentacją wad tego podejścia w porównaniu z bazą danych / repozytorium JCR

Niestety FileServlet od @BalusC koncentruje się na pobieraniu plików, podczas gdy jego odpowiedź na temat przesyłania plików pomija część dotyczącą miejsca zapisania pliku.

Preferowane byłoby rozwiązanie, które można łatwo przekształcić w implementację DB lub JCR (np. Jackrabbit ).

Mr_and_Mrs_D
źródło
Ostatni sposób, w jaki mogę to zrobić, znajduje się poniżej
Mr_and_Mrs_D,

Odpowiedzi:

165

Przechowuj go w dowolnym miejscu w dostępnej lokalizacji, z wyjątkiem folderu projektu IDE, czyli folderu wdrażania serwera, z powodów wymienionych w odpowiedzi na Przesłany obraz dostępny tylko po odświeżeniu strony :

  1. Zmiany w folderze projektu IDE nie są natychmiast odzwierciedlane w folderze roboczym serwera. W IDE jest coś w rodzaju zadania w tle, które dba o to, aby folder roboczy serwera był synchronizowany z ostatnimi aktualizacjami (w terminologii IDE nazywa się to „publikowaniem”). To jest główna przyczyna napotkanego problemu.

  2. W prawdziwym kodzie istnieją sytuacje, w których przechowywanie przesłanych plików w folderze wdrażania aplikacji sieci Web w ogóle nie zadziała. Niektóre serwery (domyślnie lub przez konfigurację) nie rozszerzają wdrożonego pliku WAR do lokalnego systemu plików dysku, ale zamiast tego w całości umieszczają go w pamięci. Nie można tworzyć nowych plików w pamięci bez zasadniczo edytowania wdrożonego pliku WAR i ponownego jego wdrożenia.

  3. Nawet jeśli serwer rozszerzy wdrożony plik WAR do lokalnego systemu plików na dysku, wszystkie nowo utworzone pliki zostaną utracone po ponownym wdrożeniu lub nawet po prostym ponownym uruchomieniu, po prostu dlatego, że te nowe pliki nie są częścią oryginalnego pliku WAR.

To naprawdę nie ma znaczenia dla mnie czy kogokolwiek innego, gdzie dokładnie na lokalnym systemie plików na dysku zostanie on zapisany, tak długo, jak nie nie zawsze używać getRealPath()metody . Stosowanie tej metody jest w każdym razie niepokojące.

Z kolei ścieżkę do miejsca przechowywania można zdefiniować na wiele sposobów. Musisz to wszystko zrobić sam . Być może jest to przyczyną twojego zamieszania, ponieważ w jakiś sposób spodziewałeś się, że serwer zrobi to wszystko automagicznie. Należy pamiętać, że @MultipartConfig(location)nie nie określenie miejsca docelowego wysyłania, ale tymczasowe miejsce składowania dla rozmiaru pliku przypadek przekracza próg przechowywania pamięci.

Tak więc ścieżkę do ostatecznego miejsca przechowywania można zdefiniować na jeden z następujących sposobów:

  • Mocno zakodowane:

      File uploads = new File("/path/to/uploads");
  • Zmienna środowiskowa za pośrednictwem SET UPLOAD_LOCATION=/path/to/uploads:

      File uploads = new File(System.getenv("UPLOAD_LOCATION"));
  • Argument maszyny wirtualnej podczas uruchamiania serwera za pośrednictwem -Dupload.location="/path/to/uploads":

      File uploads = new File(System.getProperty("upload.location"));
  • *.propertieswpis pliku jako upload.location=/path/to/uploads:

      File uploads = new File(properties.getProperty("upload.location"));
  • web.xml <context-param>z nazwą upload.locationi wartością /path/to/uploads:

      File uploads = new File(getServletContext().getInitParameter("upload.location"));
  • Jeśli istnieje, użyj lokalizacji udostępnionej przez serwer, np. W JBoss AS / WildFly :

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

Tak czy inaczej, możesz łatwo odwołać się do pliku i zapisać go w następujący sposób:

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

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

Lub, gdy chcesz automatycznie wygenerować unikalną nazwę pliku, aby uniemożliwić użytkownikom nadpisywanie istniejących plików przypadkowo o tej samej nazwie:

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

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

Jak uzyskać partw JSP / Servlet znajduje się w artykule Jak przesłać pliki na serwer za pomocą JSP / Servlet? a odpowiedź na temat sposobu uzyskania partw JSF znajduje się w artykule Jak przesłać plik za pomocą JSF 2.2 <h: inputFile>? Gdzie jest zapisany plik?

Uwaga: nie używaj, Part#write()ponieważ interpretuje ścieżkę względem tymczasowego miejsca przechowywania zdefiniowanego w @MultipartConfig(location).

Zobacz też:

BalusC
źródło
W @MultipartConfig(location)Określa tymczasowa lokalizacja storge które serwer powinien użyć, gdy rozmiar pliku przekroczy próg pamięcią, a nie stałe miejsce przechowywania, w którym bym ostatecznie jak to mają być przechowywane. Ta wartość jest domyślną ścieżką określoną przez java.io.tmpdirwłaściwość systemową. Zobacz także tę powiązaną odpowiedź na nieudaną próbę JSF: stackoverflow.com/questions/18478154/ ...
BalusC
1
Dzięki - mam nadzieję, że nie brzmię głupio, ale ten cytat z Part.write>> Pozwala to konkretnej implementacji na użycie, na przykład, zmiany nazwy pliku, jeśli to możliwe, zamiast kopiowania wszystkich bazowych danych, uzyskując w ten sposób znaczną poprawę wydajności w połączeniu z niektórymi nieznana metoda "wycinania" (w porównaniu z kopiowaniem) z, powiedzmy, pewna biblioteka apache zaoszczędziłaby mi kłopotów z samodzielnym pisaniem bajtów - i odtwarzaniem pliku już tam (zobacz także tutaj )
Mr_and_Mrs_D
Tak, jeśli jesteś już na Servlecie 3.0, możesz skorzystać z Part#write(). Zaktualizowałem to odpowiedź.
BalusC,
Bardzo dziękuję za aktualizowanie postu - czy istnieje taka właściwość dla Tomcata jak "jboss.server.data.dir"?
Mr_and_Mrs_D
1
Nie, nie ma.
BalusC,
7

Publikuję swój ostateczny sposób zrobienia tego na podstawie zaakceptowanej odpowiedzi:

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

gdzie :

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

i /WEB-INF/app.properties:

upload.location=C:/_/

HTH i jeśli znajdziesz błąd, daj mi znać

Mr_and_Mrs_D
źródło
1
A jeśli chcę niezależnego rozwiązania SO, które działa w obu przypadkach (win / ux)? Czy muszę ustawić inną ścieżkę upload.location, czy jest jakaś inna wskazówka?
pikimota