Skip to content

Commit ba52164

Browse files
committed
Provide graceful fallback for non-default NIO file systems
Closes gh-35443
1 parent 977582f commit ba52164

File tree

5 files changed

+67
-45
lines changed

5 files changed

+67
-45
lines changed

spring-core/src/main/java/org/springframework/core/io/Resource.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@ default boolean isFile() {
117117

118118
/**
119119
* Return a File handle for this resource.
120-
* @throws java.io.FileNotFoundException if the resource cannot be resolved as
121-
* absolute file path, i.e. if the resource is not available in a file system
120+
* <p>Note: This only works for files in the default file system.
121+
* @throws UnsupportedOperationException if the resource is a file but cannot be
122+
* exposed as a {@code java.io.File}; an alternative to {@code FileNotFoundException}
123+
* @throws java.io.FileNotFoundException if the resource cannot be resolved as a file
122124
* @throws IOException in case of general resolution/reading failures
123125
* @see #getInputStream()
124126
*/

spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.core.io.buffer;
1818

19-
import java.io.File;
2019
import java.io.IOException;
2120
import java.io.InputStream;
2221
import java.io.OutputStream;
@@ -223,17 +222,17 @@ public static Flux<DataBuffer> read(
223222

224223
try {
225224
if (resource.isFile()) {
226-
File file = resource.getFile();
225+
Path filePath = resource.getFile().toPath();
227226
return readAsynchronousFileChannel(
228-
() -> AsynchronousFileChannel.open(file.toPath(), StandardOpenOption.READ),
227+
() -> AsynchronousFileChannel.open(filePath, StandardOpenOption.READ),
229228
position, bufferFactory, bufferSize);
230229
}
231230
}
232-
catch (IOException ignore) {
231+
catch (IOException | UnsupportedOperationException ignore) {
233232
// fallback to resource.readableChannel(), below
234233
}
235234
Flux<DataBuffer> result = readByteChannel(resource::readableChannel, bufferFactory, bufferSize);
236-
return position == 0 ? result : skipUntilByteCount(result, position);
235+
return (position == 0 ? result : skipUntilByteCount(result, position));
237236
}
238237

239238

spring-core/src/main/java/org/springframework/util/FileSystemUtils.java

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,12 @@ public static boolean deleteRecursively(@Nullable Path root) throws IOException
8686

8787
Files.walkFileTree(root, new SimpleFileVisitor<>() {
8888
@Override
89-
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
89+
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
9090
Files.delete(file);
9191
return FileVisitResult.CONTINUE;
9292
}
93-
9493
@Override
95-
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
94+
public FileVisitResult postVisitDirectory(Path dir, IOException ex) throws IOException {
9695
Files.delete(dir);
9796
return FileVisitResult.CONTINUE;
9897
}
@@ -127,19 +126,34 @@ public static void copyRecursively(Path src, Path dest) throws IOException {
127126
BasicFileAttributes srcAttr = Files.readAttributes(src, BasicFileAttributes.class);
128127

129128
if (srcAttr.isDirectory()) {
130-
Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
131-
@Override
132-
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
133-
Files.createDirectories(dest.resolve(src.relativize(dir)));
134-
return FileVisitResult.CONTINUE;
135-
}
136-
137-
@Override
138-
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
139-
Files.copy(file, dest.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
140-
return FileVisitResult.CONTINUE;
141-
}
142-
});
129+
if (src.getClass() == dest.getClass()) { // dest.resolve(Path) only works for same Path type
130+
Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
131+
@Override
132+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException {
133+
Files.createDirectories(dest.resolve(src.relativize(dir)));
134+
return FileVisitResult.CONTINUE;
135+
}
136+
@Override
137+
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
138+
Files.copy(file, dest.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
139+
return FileVisitResult.CONTINUE;
140+
}
141+
});
142+
}
143+
else { // use dest.resolve(String) for different Path types
144+
Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
145+
@Override
146+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException {
147+
Files.createDirectories(dest.resolve(src.relativize(dir).toString()));
148+
return FileVisitResult.CONTINUE;
149+
}
150+
@Override
151+
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
152+
Files.copy(file, dest.resolve(src.relativize(file).toString()), StandardCopyOption.REPLACE_EXISTING);
153+
return FileVisitResult.CONTINUE;
154+
}
155+
});
156+
}
143157
}
144158
else if (srcAttr.isRegularFile()) {
145159
Files.copy(src, dest);

spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,29 @@
1717
package org.springframework.util;
1818

1919
import java.io.File;
20+
import java.net.URI;
21+
import java.nio.file.FileSystem;
22+
import java.nio.file.FileSystems;
23+
import java.nio.file.Path;
24+
import java.util.Map;
2025

21-
import org.junit.jupiter.api.AfterEach;
2226
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.io.TempDir;
2328

2429
import static org.assertj.core.api.Assertions.assertThat;
2530

2631
/**
32+
* Tests for {@link FileSystemUtils}.
33+
*
2734
* @author Rob Harrop
35+
* @author Sam Brannen
36+
* @author Juergen Hoeller
2837
*/
2938
class FileSystemUtilsTests {
3039

3140
@Test
32-
void deleteRecursively() throws Exception {
33-
File root = new File("./tmp/root");
41+
void deleteRecursively(@TempDir File tempDir) throws Exception {
42+
File root = new File(tempDir, "root");
3443
File child = new File(root, "child");
3544
File grandchild = new File(child, "grandchild");
3645

@@ -53,8 +62,8 @@ void deleteRecursively() throws Exception {
5362
}
5463

5564
@Test
56-
void copyRecursively() throws Exception {
57-
File src = new File("./tmp/src");
65+
void copyRecursively(@TempDir File tempDir) throws Exception {
66+
File src = new File(tempDir, "src");
5867
File child = new File(src, "child");
5968
File grandchild = new File(child, "grandchild");
6069

@@ -68,27 +77,25 @@ void copyRecursively() throws Exception {
6877
assertThat(grandchild).exists();
6978
assertThat(bar).exists();
7079

71-
File dest = new File("./dest");
80+
File dest = new File(tempDir, "/dest");
7281
FileSystemUtils.copyRecursively(src, dest);
7382

7483
assertThat(dest).exists();
75-
assertThat(new File(dest, child.getName())).exists();
84+
assertThat(new File(dest, "child")).exists();
85+
assertThat(new File(dest, "child/bar.txt")).exists();
7686

77-
FileSystemUtils.deleteRecursively(src);
78-
assertThat(src).doesNotExist();
79-
}
87+
URI uri = URI.create("jar:file:/" + dest.toString().replace('\\', '/') + "/archive.zip");
88+
Map<String, String> env = Map.of("create", "true");
89+
FileSystem zipfs = FileSystems.newFileSystem(uri, env);
90+
Path ziproot = zipfs.getPath("/");
91+
FileSystemUtils.copyRecursively(src.toPath(), ziproot);
8092

93+
assertThat(zipfs.getPath("/child")).exists();
94+
assertThat(zipfs.getPath("/child/bar.txt")).exists();
8195

82-
@AfterEach
83-
void tearDown() {
84-
File tmp = new File("./tmp");
85-
if (tmp.exists()) {
86-
FileSystemUtils.deleteRecursively(tmp);
87-
}
88-
File dest = new File("./dest");
89-
if (dest.exists()) {
90-
FileSystemUtils.deleteRecursively(dest);
91-
}
96+
zipfs.close();
97+
FileSystemUtils.deleteRecursively(src);
98+
assertThat(src).doesNotExist();
9299
}
93100

94101
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ private static Mono<Void> zeroCopy(Resource resource, @Nullable ResourceRegion r
203203
}
204204
return zeroCopyHttpOutputMessage.writeWith(file, pos, count);
205205
}
206-
catch (IOException ex) {
207-
// should not happen
206+
catch (IOException | UnsupportedOperationException ignore) {
207+
// returning null below leads to fallback code path
208208
}
209209
}
210210
return null;

0 commit comments

Comments
 (0)