Skip to content

Commit 332257d

Browse files
poutsmalxbzmy
authored andcommitted
Ensure DefaultPartHttpMessageReader temp directories do not collide
This commit makes sure that the DefaultPartHttpMessageReader uses a random temporary directory to store uploaded files, so that two instances do not collide. See spring-projectsgh-26931
1 parent 4b0e1ff commit 332257d

File tree

4 files changed

+217
-25
lines changed

4 files changed

+217
-25
lines changed

spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
import java.io.IOException;
2020
import java.nio.charset.Charset;
2121
import java.nio.charset.StandardCharsets;
22-
import java.nio.file.Files;
2322
import java.nio.file.Path;
24-
import java.nio.file.Paths;
2523
import java.util.Collections;
2624
import java.util.List;
2725
import java.util.Map;
@@ -63,8 +61,6 @@
6361
*/
6462
public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader<Part> {
6563

66-
private static final String IDENTIFIER = "spring-multipart";
67-
6864
private int maxInMemorySize = 256 * 1024;
6965

7066
private int maxHeadersSize = 8 * 1024;
@@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
7773

7874
private Scheduler blockingOperationScheduler = Schedulers.boundedElastic();
7975

80-
private Mono<Path> fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache();
76+
private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler);
8177

8278
private Charset headersCharset = StandardCharsets.UTF_8;
8379

@@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) {
147143
*/
148144
public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException {
149145
Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null");
150-
if (!Files.exists(fileStorageDirectory)) {
151-
Files.createDirectory(fileStorageDirectory);
152-
}
153-
this.fileStorageDirectory = Mono.just(fileStorageDirectory);
146+
this.fileStorage = FileStorage.fromPath(fileStorageDirectory);
154147
}
155148

156149
/**
@@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler)
168161
this.blockingOperationScheduler = blockingOperationScheduler;
169162
}
170163

164+
private Scheduler getBlockingOperationScheduler() {
165+
return this.blockingOperationScheduler;
166+
}
167+
171168
/**
172169
* When set to {@code true}, the {@linkplain Part#content() part content}
173170
* is streamed directly from the parsed input buffer stream, and not stored
@@ -230,7 +227,7 @@ public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage mess
230227
this.maxHeadersSize, this.headersCharset);
231228

232229
return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart,
233-
this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler);
230+
this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler);
234231
});
235232
}
236233

@@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) {
250247
return null;
251248
}
252249

253-
@SuppressWarnings("BlockingMethodInNonBlockingContext")
254-
private Mono<Path> defaultFileStorageDirectory() {
255-
return Mono.fromCallable(() -> {
256-
Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER);
257-
if (!Files.exists(tempDirectory)) {
258-
Files.createDirectory(tempDirectory);
259-
}
260-
return tempDirectory;
261-
}).subscribeOn(this.blockingOperationScheduler);
262-
263-
}
264-
265250
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.codec.multipart;
18+
19+
import java.io.IOException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.util.function.Supplier;
23+
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
26+
import reactor.core.publisher.Mono;
27+
import reactor.core.scheduler.Scheduler;
28+
29+
/**
30+
* Represents a directory used to store parts larger than
31+
* {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}.
32+
*
33+
* @author Arjen Poutsma
34+
* @since 5.3.7
35+
*/
36+
abstract class FileStorage {
37+
38+
private static final Log logger = LogFactory.getLog(FileStorage.class);
39+
40+
41+
protected FileStorage() {
42+
}
43+
44+
/**
45+
* Get the mono of the directory to store files in.
46+
*/
47+
public abstract Mono<Path> directory();
48+
49+
50+
/**
51+
* Create a new {@code FileStorage} from a user-specified path. Creates the
52+
* path if it does not exist.
53+
*/
54+
public static FileStorage fromPath(Path path) throws IOException {
55+
if (!Files.exists(path)) {
56+
Files.createDirectory(path);
57+
}
58+
return new PathFileStorage(path);
59+
}
60+
61+
/**
62+
* Create a new {@code FileStorage} based a on a temporary directory.
63+
* @param scheduler scheduler to use for blocking operations
64+
*/
65+
public static FileStorage tempDirectory(Supplier<Scheduler> scheduler) {
66+
return new TempFileStorage(scheduler);
67+
}
68+
69+
70+
private static final class PathFileStorage extends FileStorage {
71+
72+
private final Mono<Path> directory;
73+
74+
public PathFileStorage(Path directory) {
75+
this.directory = Mono.just(directory);
76+
}
77+
78+
@Override
79+
public Mono<Path> directory() {
80+
return this.directory;
81+
}
82+
}
83+
84+
85+
private static final class TempFileStorage extends FileStorage {
86+
87+
private static final String IDENTIFIER = "spring-multipart-";
88+
89+
private final Supplier<Scheduler> scheduler;
90+
91+
private volatile Mono<Path> directory = tempDirectory();
92+
93+
94+
public TempFileStorage(Supplier<Scheduler> scheduler) {
95+
this.scheduler = scheduler;
96+
}
97+
98+
@Override
99+
public Mono<Path> directory() {
100+
return this.directory
101+
.flatMap(this::createNewDirectoryIfDeleted)
102+
.subscribeOn(this.scheduler.get());
103+
}
104+
105+
private Mono<Path> createNewDirectoryIfDeleted(Path directory) {
106+
if (!Files.exists(directory)) {
107+
// Some daemons remove temp directories. Let's create a new one.
108+
Mono<Path> newDirectory = tempDirectory();
109+
this.directory = newDirectory;
110+
return newDirectory;
111+
}
112+
else {
113+
return Mono.just(directory);
114+
}
115+
}
116+
117+
private static Mono<Path> tempDirectory() {
118+
return Mono.fromCallable(() -> {
119+
Path directory = Files.createTempDirectory(IDENTIFIER);
120+
if (logger.isDebugEnabled()) {
121+
logger.debug("Created temporary storage directory: " + directory);
122+
}
123+
return directory;
124+
}).cache();
125+
}
126+
}
127+
128+
}

spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -578,9 +578,6 @@ public void createFile() {
578578

579579
private WritingFileState createFileState(Path directory) {
580580
try {
581-
if (!Files.exists(directory)) {
582-
Files.createDirectory(directory);
583-
}
584581
Path tempFile = Files.createTempFile(directory, null, ".multipart");
585582
if (logger.isTraceEnabled()) {
586583
logger.trace("Storing multipart data in file " + tempFile);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.codec.multipart;
18+
19+
import java.io.IOException;
20+
import java.io.UncheckedIOException;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
24+
import org.junit.jupiter.api.Test;
25+
import reactor.core.publisher.Mono;
26+
import reactor.core.scheduler.Schedulers;
27+
import reactor.test.StepVerifier;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
/**
32+
* @author Arjen Poutsma
33+
*/
34+
class FileStorageTests {
35+
36+
@Test
37+
void fromPath() throws IOException {
38+
Path path = Files.createTempFile("spring", "test");
39+
FileStorage storage = FileStorage.fromPath(path);
40+
41+
Mono<Path> directory = storage.directory();
42+
StepVerifier.create(directory)
43+
.expectNext(path)
44+
.verifyComplete();
45+
}
46+
47+
@Test
48+
void tempDirectory() {
49+
FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic);
50+
51+
Mono<Path> directory = storage.directory();
52+
StepVerifier.create(directory)
53+
.consumeNextWith(path -> {
54+
assertThat(path).exists();
55+
StepVerifier.create(directory)
56+
.expectNext(path)
57+
.verifyComplete();
58+
})
59+
.verifyComplete();
60+
}
61+
62+
@Test
63+
void tempDirectoryDeleted() {
64+
FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic);
65+
66+
Mono<Path> directory = storage.directory();
67+
StepVerifier.create(directory)
68+
.consumeNextWith(path1 -> {
69+
try {
70+
Files.delete(path1);
71+
StepVerifier.create(directory)
72+
.consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1))
73+
.verifyComplete();
74+
}
75+
catch (IOException ex) {
76+
throw new UncheckedIOException(ex);
77+
}
78+
})
79+
.verifyComplete();
80+
}
81+
82+
}

0 commit comments

Comments
 (0)