All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.http.MultiPartFormData Maven / Gradle / Ivy

//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.http;

import java.io.Closeable;
import java.nio.ByteBuffer;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.nio.charset.StandardCharsets.US_ASCII;

/**
 * 

A {@link CompletableFuture} that is completed when a multipart/form-data content * has been parsed asynchronously from a {@link Content.Source}.

*

Once the parsing of the multipart/form-data content completes successfully, * objects of this class are completed with a {@link Parts} object.

*

Objects of this class may be configured to save multipart files in a configurable * directory, and configure the max size of such files, etc.

*

Typical usage:

*
{@code
 * // Some headers that include Content-Type.
 * HttpFields headers = ...;
 * String boundary = MultiPart.extractBoundary(headers.get(HttpHeader.CONTENT_TYPE));
 *
 * // Some multipart/form-data content.
 * Content.Source content = ...;
 *
 * // Create and configure MultiPartFormData.
 * MultiPartFormData formData = new MultiPartFormData(boundary);
 * // Where to store the files.
 * formData.setFilesDirectory(Path.of("/tmp"));
 * // Max 1 MiB files.
 * formData.setMaxFileSize(1024 * 1024);
 *
 * // Parse the content.
 * formData.parse(content)
 *     // When complete, use the parts.
 *     .thenAccept(parts -> ...);
 * }
* * @see Parts */ public class MultiPartFormData { private static final Logger LOG = LoggerFactory.getLogger(MultiPartFormData.class); private MultiPartFormData() { } public static CompletableFuture from(Attributes attributes, String boundary, Function> parse) { @SuppressWarnings("unchecked") CompletableFuture futureParts = (CompletableFuture)attributes.getAttribute(MultiPartFormData.class.getName()); if (futureParts == null) { futureParts = parse.apply(new Parser(boundary)); attributes.setAttribute(MultiPartFormData.class.getName(), futureParts); } return futureParts; } /** *

An ordered list of {@link MultiPart.Part}s that can * be accessed by index or by name, or iterated over.

*/ public static class Parts implements Iterable, Closeable { private final List parts; private Parts(List parts) { this.parts = parts; } /** *

Returns the {@link MultiPart.Part} at the given index, a number * between {@code 0} included and the value returned by {@link #size()} * excluded.

* * @param index the index of the {@code MultiPart.Part} to return * @return the {@code MultiPart.Part} at the given index */ public MultiPart.Part get(int index) { return parts.get(index); } /** *

Returns the first {@link MultiPart.Part} with the given name, or * {@code null} if no {@code MultiPart.Part} with that name exists.

* * @param name the {@code MultiPart.Part} name * @return the first {@code MultiPart.Part} with the given name, or {@code null} */ public MultiPart.Part getFirst(String name) { return parts.stream() .filter(part -> part.getName().equals(name)) .findFirst() .orElse(null); } /** *

Returns all the {@link MultiPart.Part}s with the given name.

* * @param name the {@code MultiPart.Part}s name * @return all the {@code MultiPart.Part}s with the given name */ public List getAll(String name) { return parts.stream() .filter(part -> part.getName().equals(name)) .toList(); } /** * @return the number of parts * @see #get(int) */ public int size() { return parts.size(); } @Override public Iterator iterator() { return parts.iterator(); } @Override public void close() { for (MultiPart.Part p : parts) { IO.close(p); } } } /** *

The multipart/form-data specific content source.

* * @see MultiPart.AbstractContentSource */ public static class ContentSource extends MultiPart.AbstractContentSource { public ContentSource(String boundary) { super(boundary); } protected HttpFields customizePartHeaders(MultiPart.Part part) { HttpFields headers = super.customizePartHeaders(part); if (headers.contains(HttpHeader.CONTENT_DISPOSITION)) return headers; String value = "form-data"; String name = part.getName(); if (name != null) value += "; name=" + QuotedCSV.quote(name); String fileName = part.getFileName(); if (fileName != null) value += "; filename=" + QuotedCSV.quote(fileName); return HttpFields.build(headers).put(HttpHeader.CONTENT_DISPOSITION, value); } } public static class Parser { private final PartsListener listener = new PartsListener(); private final MultiPart.Parser parser; private boolean useFilesForPartsWithoutFileName; private Path filesDirectory; private long maxFileSize = -1; private long maxMemoryFileSize; private long maxLength = -1; private long length; private Parts parts; public Parser(String boundary) { parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener); } public CompletableFuture parse(Content.Source content) { ContentSourceCompletableFuture futureParts = new ContentSourceCompletableFuture<>(content) { @Override protected Parts parse(Content.Chunk chunk) throws Throwable { if (listener.isFailed()) throw listener.failure; length += chunk.getByteBuffer().remaining(); long max = getMaxLength(); if (max >= 0 && length > max) throw new IllegalStateException("max length exceeded: %d".formatted(max)); parser.parse(chunk); if (listener.isFailed()) throw listener.failure; return parts; } @Override public boolean completeExceptionally(Throwable failure) { boolean failed = super.completeExceptionally(failure); if (failed) listener.fail(failure); return failed; } }; futureParts.parse(); return futureParts; } /** * @return the boundary string */ public String getBoundary() { return parser.getBoundary(); } /** *

Returns the default charset as specified by * RFC 7578, section 4.6, * that is the charset specified by the part named {@code _charset_}.

*

If that part is not present, returns {@code null}.

* * @return the default charset specified by the {@code _charset_} part, * or null if that part is not present */ public Charset getDefaultCharset() { return listener.getDefaultCharset(); } /** * @return the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length */ public int getPartHeadersMaxLength() { return parser.getPartHeadersMaxLength(); } /** * @param partHeadersMaxLength the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length */ public void setPartHeadersMaxLength(int partHeadersMaxLength) { parser.setPartHeadersMaxLength(partHeadersMaxLength); } /** * @return whether parts without fileName may be stored as files */ public boolean isUseFilesForPartsWithoutFileName() { return useFilesForPartsWithoutFileName; } /** * @param useFilesForPartsWithoutFileName whether parts without fileName may be stored as files */ public void setUseFilesForPartsWithoutFileName(boolean useFilesForPartsWithoutFileName) { this.useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName; } /** * @return the directory where files are saved */ public Path getFilesDirectory() { return filesDirectory; } /** *

Sets the directory where the files uploaded in the parts will be saved.

* * @param filesDirectory the directory where files are saved */ public void setFilesDirectory(Path filesDirectory) { this.filesDirectory = filesDirectory; } /** * @return the maximum file size in bytes, or -1 for unlimited file size */ public long getMaxFileSize() { return maxFileSize; } /** * @param maxFileSize the maximum file size in bytes, or -1 for unlimited file size */ public void setMaxFileSize(long maxFileSize) { this.maxFileSize = maxFileSize; } /** * @return the maximum memory file size in bytes, or -1 for unlimited memory file size */ public long getMaxMemoryFileSize() { return maxMemoryFileSize; } /** *

Sets the maximum memory file size in bytes, after which files will be saved * in the directory specified by {@link #setFilesDirectory(Path)}.

*

Use value {@code 0} to always save the files in the directory.

*

Use value {@code -1} to never save the files in the directory.

* * @param maxMemoryFileSize the maximum memory file size in bytes, or -1 for unlimited memory file size */ public void setMaxMemoryFileSize(long maxMemoryFileSize) { this.maxMemoryFileSize = maxMemoryFileSize; } /** * @return the maximum length in bytes of the whole multipart content, or -1 for unlimited length */ public long getMaxLength() { return maxLength; } /** * @param maxLength the maximum length in bytes of the whole multipart content, or -1 for unlimited length */ public void setMaxLength(long maxLength) { this.maxLength = maxLength; } /** * @return the maximum number of parts that can be parsed from the multipart content. */ public long getMaxParts() { return parser.getMaxParts(); } /** * @param maxParts the maximum number of parts that can be parsed from the multipart content. */ public void setMaxParts(long maxParts) { parser.setMaxParts(maxParts); } // Only used for testing. int getPartsSize() { return listener.getPartsSize(); } private class PartsListener extends MultiPart.AbstractPartsListener { private final AutoLock lock = new AutoLock(); private final List parts = new ArrayList<>(); private final List partChunks = new ArrayList<>(); private long fileSize; private long memoryFileSize; private Path filePath; private SeekableByteChannel fileChannel; private Throwable failure; @Override public void onPartContent(Content.Chunk chunk) { ByteBuffer buffer = chunk.getByteBuffer(); String fileName = getFileName(); if (fileName != null || isUseFilesForPartsWithoutFileName()) { long maxFileSize = getMaxFileSize(); fileSize += buffer.remaining(); if (maxFileSize >= 0 && fileSize > maxFileSize) { onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize))); return; } long maxMemoryFileSize = getMaxMemoryFileSize(); if (maxMemoryFileSize >= 0) { memoryFileSize += buffer.remaining(); if (memoryFileSize > maxMemoryFileSize) { try { // Must save to disk. if (ensureFileChannel()) { // Write existing memory chunks. List partChunks; try (AutoLock ignored = lock.lock()) { partChunks = List.copyOf(this.partChunks); } for (Content.Chunk c : partChunks) { write(c.getByteBuffer()); } } write(buffer); if (chunk.isLast()) close(); } catch (Throwable x) { onFailure(x); } try (AutoLock ignored = lock.lock()) { partChunks.forEach(Content.Chunk::release); partChunks.clear(); } return; } } } // Retain the chunk because it is stored for later use. chunk.retain(); try (AutoLock ignored = lock.lock()) { partChunks.add(chunk); } } private void write(ByteBuffer buffer) throws Exception { int remaining = buffer.remaining(); while (remaining > 0) { SeekableByteChannel channel = fileChannel(); if (channel == null) throw new IllegalStateException(); int written = channel.write(buffer); if (written == 0) throw new NonWritableChannelException(); remaining -= written; } } private void close() { try { Closeable closeable = fileChannel(); if (closeable != null) closeable.close(); } catch (Throwable x) { onFailure(x); } } @Override public void onPart(String name, String fileName, HttpFields headers) { fileSize = 0; memoryFileSize = 0; try (AutoLock ignored = lock.lock()) { MultiPart.Part part; if (fileChannel != null) part = new MultiPart.PathPart(name, fileName, headers, filePath); else part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)); // Reset part-related state. filePath = null; fileChannel = null; partChunks.forEach(Content.Chunk::release); partChunks.clear(); // Store the new part. parts.add(part); } } @Override public void onComplete() { super.onComplete(); List result; try (AutoLock ignored = lock.lock()) { result = List.copyOf(parts); Parser.this.parts = new Parts(result); } } Charset getDefaultCharset() { try (AutoLock ignored = lock.lock()) { return parts.stream() .filter(part -> "_charset_".equals(part.getName())) .map(part -> part.getContentAsString(US_ASCII)) .map(Charset::forName) .findFirst() .orElse(null); } } int getPartsSize() { try (AutoLock ignored = lock.lock()) { return parts.size(); } } @Override public void onFailure(Throwable failure) { fail(failure); } private void fail(Throwable cause) { List partsToFail; try (AutoLock ignored = lock.lock()) { if (failure != null) return; failure = cause; partsToFail = List.copyOf(parts); parts.clear(); partChunks.forEach(Content.Chunk::release); partChunks.clear(); } partsToFail.forEach(p -> p.fail(cause)); close(); delete(); } private SeekableByteChannel fileChannel() { try (AutoLock ignored = lock.lock()) { return fileChannel; } } private void delete() { try { Path path = null; try (AutoLock ignored = lock.lock()) { if (filePath != null) path = filePath; filePath = null; fileChannel = null; } if (path != null) Files.delete(path); } catch (Throwable x) { if (LOG.isTraceEnabled()) LOG.trace("IGNORED", x); } } private boolean isFailed() { try (AutoLock ignored = lock.lock()) { return failure != null; } } private boolean ensureFileChannel() { try (AutoLock ignored = lock.lock()) { if (fileChannel != null) return false; createFileChannel(); return true; } } private void createFileChannel() { try (AutoLock ignored = lock.lock()) { Path directory = getFilesDirectory(); Files.createDirectories(directory); String fileName = "MultiPart"; filePath = Files.createTempFile(directory, fileName, ""); fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.APPEND); } catch (Throwable x) { onFailure(x); } } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy