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

reactor.netty.http.server.HttpServerFormDecoderProvider Maven / Gradle / Ivy

/*
 * Copyright (c) 2021-2023 VMware, Inc. or its affiliates, All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package reactor.netty.http.server;

import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpData;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder;
import io.netty.handler.codec.http.multipart.HttpPostStandardRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.util.annotation.Nullable;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * A configuration builder to fine tune the HTTP form decoder.
 *
 * @author Violeta Georgieva
 * @since 1.0.11
 */
public final class HttpServerFormDecoderProvider {

	public interface Builder {

		/**
		 * Sets the directory where to store disk {@link Attribute}/{@link FileUpload}.
		 * Default to generated temp directory.
		 *
		 * @param baseDirectory directory where to store disk {@link Attribute}/{@link FileUpload}
		 * @return {@code this}
		 */
		Builder baseDirectory(Path baseDirectory);

		/**
		 * Set the {@link Charset} for {@link Attribute}/{@link FileUpload}. Default to {@link StandardCharsets#UTF_8}.
		 *
		 * @param charset the charset for {@link Attribute}/{@link FileUpload}
		 * @return {@code this}
		 */
		Builder charset(Charset charset);

		/**
		 * Sets the maximum in-memory size per {@link Attribute}/{@link FileUpload} i.e. the data is written
		 * on disk if the size is greater than {@code maxInMemorySize}, else it is in memory.
		 * Default to {@link DefaultHttpDataFactory#MINSIZE}.
		 * 

Note: *

    *
  • If set to {@code -1} the entire contents is stored in memory
  • *
  • If set to {@code 0} the entire contents is stored on disk
  • *
* * @param maxInMemorySize the maximum in-memory size * @return {@code this} */ Builder maxInMemorySize(long maxInMemorySize); /** * Set the maximum size per {@link Attribute}/{@link FileUpload}. When the limit is reached, an exception is raised. * Default to {@link DefaultHttpDataFactory#MAXSIZE} - unlimited. *

Note: If set to {@code -1} this means no limitation. * * @param maxSize the maximum size allowed for an individual attribute/fileUpload * @return {@code this} */ Builder maxSize(long maxSize); /** * Sets the scheduler to be used for offloading disk operations in the decoding phase. * Default to {@link Schedulers#boundedElastic()} * * @param scheduler the scheduler to be used for offloading disk operations in the decoding phase * @return {@code this} */ Builder scheduler(Scheduler scheduler); /** * When set to {@code true}, the data is streamed directly from the parsed input buffer stream, * which means it is not stored either in memory or file. * When {@code false}, parts are backed by in-memory and/or file storage. Default to {@code false}. *

NOTE that with streaming enabled, the provided {@link Attribute}/{@link FileUpload} * might not be in a complete state i.e. {@link HttpData#isCompleted()} has to be checked. *

Also note that enabling this property effectively ignores * {@link #maxInMemorySize(long)}, * {@link #baseDirectory(Path)}, and * {@link #scheduler(Scheduler)}. */ Builder streaming(boolean enable); } final Path baseDirectory; final Charset charset; final long maxInMemorySize; final long maxSize; final Scheduler scheduler; final boolean streaming; private volatile Mono defaultTempDirectory = createDefaultTempDirectory(); HttpServerFormDecoderProvider(Build build) { this.baseDirectory = build.baseDirectory; this.charset = build.charset; this.maxInMemorySize = !build.streaming ? build.maxInMemorySize : -1; this.maxSize = build.maxSize; this.scheduler = build.scheduler; this.streaming = build.streaming; } /** * Returns the configured directory where to store disk {@link Attribute}/{@link FileUpload}. * * @return the configured directory where to store disk {@link Attribute}/{@link FileUpload} * @see Builder#baseDirectory(Path) */ @Nullable public Path baseDirectory() { return baseDirectory; } /** * Returns the configured charset for {@link Attribute}/{@link FileUpload}. * * @return the configured charset for {@link Attribute}/{@link FileUpload} * @see Builder#charset(Charset) */ public Charset charset() { return charset; } /** * Returns the configured maximum size after which an {@link Attribute}/{@link FileUpload} starts being stored on disk rather than in memory. * * @return the configured maximum size after which an {@link Attribute}/{@link FileUpload} starts being stored on disk rather than in memory * @see Builder#maxInMemorySize(long) */ public long maxInMemorySize() { return maxInMemorySize; } /** * Returns the configured maximum allowed size of individual {@link Attribute}/{@link FileUpload}. * * @return the configured maximum allowed size of individual {@link Attribute}/{@link FileUpload} * @see Builder#maxSize(long) */ public long maxSize() { return maxSize; } /** * Returns the configured scheduler to be used for offloading disk operations in the decoding phase. * * @return the configured scheduler to be used for offloading disk operations in the decoding phase * @see Builder#scheduler(Scheduler) */ public Scheduler scheduler() { return scheduler; } /** * Returns whether the streaming mode is enabled. * * @return whether the streaming mode is enabled * @see Builder#streaming(boolean) */ public boolean streaming() { return streaming; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof HttpServerFormDecoderProvider)) { return false; } HttpServerFormDecoderProvider that = (HttpServerFormDecoderProvider) o; return maxInMemorySize == that.maxInMemorySize && maxSize == that.maxSize && streaming == that.streaming && Objects.equals(baseDirectory, that.baseDirectory) && charset.equals(that.charset) && scheduler.equals(that.scheduler); } @Override public int hashCode() { int result = 1; result = 31 * result + Objects.hashCode(baseDirectory); result = 31 * result + Objects.hashCode(charset); result = 31 * result + Long.hashCode(maxInMemorySize); result = 31 * result + Long.hashCode(maxSize); result = 31 * result + Objects.hashCode(scheduler); result = 31 * result + Boolean.hashCode(streaming); return result; } Mono createDefaultTempDirectory() { return Mono.fromCallable(() -> Files.createTempDirectory(DEFAULT_TEMP_DIRECTORY_PREFIX)) .cache(); } Mono defaultTempDirectory() { return defaultTempDirectory .flatMap(dir -> { if (!Files.exists(dir)) { Mono newDirectory = createDefaultTempDirectory(); defaultTempDirectory = newDirectory; return newDirectory; } else { return Mono.just(dir); } }) .subscribeOn(scheduler); } Mono newHttpPostRequestDecoder(HttpRequest request, boolean isMultipart) { if (maxInMemorySize > -1) { Mono directoryMono; if (baseDirectory == null) { directoryMono = defaultTempDirectory(); } else { directoryMono = Mono.just(baseDirectory); } return directoryMono.map(directory -> createNewHttpPostRequestDecoder(request, isMultipart, directory)); } else { return Mono.just(createNewHttpPostRequestDecoder(request, isMultipart, null)); } } ReactorNettyHttpPostRequestDecoder createNewHttpPostRequestDecoder(HttpRequest request, boolean isMultipart, @Nullable Path baseDirectory) { DefaultHttpDataFactory factory = maxInMemorySize > 0 ? new DefaultHttpDataFactory(maxInMemorySize, charset) : new DefaultHttpDataFactory(maxInMemorySize == 0, charset); factory.setMaxLimit(maxSize); if (baseDirectory != null) { factory.setBaseDir(baseDirectory.toFile().getAbsolutePath()); } return isMultipart ? new ReactorNettyHttpPostMultipartRequestDecoder(factory, request) : new ReactorNettyHttpPostStandardRequestDecoder(factory, request); } static final HttpServerFormDecoderProvider DEFAULT_FORM_DECODER_SPEC = new HttpServerFormDecoderProvider.Build().build(); static final String DEFAULT_TEMP_DIRECTORY_PREFIX = "RN_form_"; interface ReactorNettyHttpPostRequestDecoder extends InterfaceHttpPostRequestDecoder { void cleanCurrentHttpData(boolean onlyCompleted); List currentHttpData(boolean onlyCompleted); } static final class Build implements Builder { static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; static final long DEFAULT_MAX_IN_MEMORY_SIZE = DefaultHttpDataFactory.MINSIZE; static final long DEFAULT_MAX_SIZE = DefaultHttpDataFactory.MAXSIZE; static final Scheduler DEFAULT_SCHEDULER = Schedulers.boundedElastic(); static final boolean DEFAULT_STREAMING = false; Path baseDirectory; Charset charset = DEFAULT_CHARSET; long maxInMemorySize = DEFAULT_MAX_IN_MEMORY_SIZE; long maxSize = DEFAULT_MAX_SIZE; Scheduler scheduler = DEFAULT_SCHEDULER; boolean streaming = DEFAULT_STREAMING; @Override public Builder baseDirectory(Path baseDirectory) { this.baseDirectory = Objects.requireNonNull(baseDirectory, "baseDirectory"); return this; } @Override public Builder charset(Charset charset) { this.charset = Objects.requireNonNull(charset, "charset"); return this; } @Override public Builder maxInMemorySize(long maxInMemorySize) { if (maxInMemorySize < -1) { throw new IllegalArgumentException("Maximum in-memory size must be greater or equal to -1"); } this.maxInMemorySize = maxInMemorySize; return this; } @Override public Builder maxSize(long maxSize) { if (maxSize < -1) { throw new IllegalArgumentException("Maximum size must be be greater or equal to -1"); } this.maxSize = maxSize; return this; } @Override public Builder scheduler(Scheduler scheduler) { this.scheduler = Objects.requireNonNull(scheduler, "scheduler"); return this; } @Override public Builder streaming(boolean enable) { this.streaming = enable; return this; } HttpServerFormDecoderProvider build() { return new HttpServerFormDecoderProvider(this); } } static final class ReactorNettyHttpPostMultipartRequestDecoder extends HttpPostMultipartRequestDecoder implements ReactorNettyHttpPostRequestDecoder { /** * Current {@link HttpData} from the body (only the completed {@link HttpData}). */ final List currentCompletedHttpData = new ArrayList<>(); ReactorNettyHttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request) { super(factory, request); } @Override protected void addHttpData(InterfaceHttpData data) { if (data instanceof HttpData) { currentCompletedHttpData.add((HttpData) data); } } @Override public void cleanCurrentHttpData(boolean onlyCompleted) { for (HttpData data : currentCompletedHttpData) { removeHttpDataFromClean(data); data.release(); } currentCompletedHttpData.clear(); if (!onlyCompleted) { InterfaceHttpData partial = currentPartialHttpData(); if (partial instanceof HttpData) { ((HttpData) partial).delete(); } } } @Override public List currentHttpData(boolean onlyCompleted) { if (!onlyCompleted) { InterfaceHttpData partial = currentPartialHttpData(); if (partial instanceof HttpData) { currentCompletedHttpData.add(((HttpData) partial).retainedDuplicate()); } } return currentCompletedHttpData; } @Override public void destroy() { super.destroy(); InterfaceHttpData partial = currentPartialHttpData(); if (partial != null) { partial.release(); } } } static final class ReactorNettyHttpPostStandardRequestDecoder extends HttpPostStandardRequestDecoder implements ReactorNettyHttpPostRequestDecoder { /** * Current {@link HttpData} from the body (only the completed {@link HttpData}). */ final List currentCompletedHttpData = new ArrayList<>(); ReactorNettyHttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request) { super(factory, request); } @Override protected void addHttpData(InterfaceHttpData data) { if (data instanceof HttpData) { currentCompletedHttpData.add((HttpData) data); } } @Override public void cleanCurrentHttpData(boolean onlyCompleted) { for (HttpData data : currentCompletedHttpData) { removeHttpDataFromClean(data); data.release(); } currentCompletedHttpData.clear(); if (!onlyCompleted) { InterfaceHttpData partial = currentPartialHttpData(); if (partial instanceof HttpData) { ((HttpData) partial).delete(); } } } @Override public List currentHttpData(boolean onlyCompleted) { if (!onlyCompleted) { InterfaceHttpData partial = currentPartialHttpData(); if (partial instanceof HttpData) { currentCompletedHttpData.add(((HttpData) partial).retainedDuplicate()); } } return currentCompletedHttpData; } @Override public void destroy() { super.destroy(); InterfaceHttpData partial = currentPartialHttpData(); if (partial != null) { partial.release(); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy