Skip to content

Commit afd905b

Browse files
committed
Returned file is not released before onComplete invocation
- fix #3765
1 parent bb1b6b8 commit afd905b

File tree

5 files changed

+236
-125
lines changed

5 files changed

+236
-125
lines changed

jooby/src/main/java/io/jooby/FileDownload.java

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.nio.file.Paths;
1717

1818
import edu.umd.cs.findbugs.annotations.NonNull;
19+
import edu.umd.cs.findbugs.annotations.Nullable;
1920

2021
/**
2122
* Represents a file download.
@@ -60,6 +61,10 @@ public enum Mode {
6061

6162
private final InputStream content;
6263

64+
private boolean deleteOnComplete;
65+
66+
private Path file;
67+
6368
/**
6469
* Creates a new file attachment.
6570
*
@@ -120,6 +125,7 @@ public FileDownload(Mode mode, @NonNull byte[] content, @NonNull String fileName
120125
*/
121126
public FileDownload(Mode mode, @NonNull Path file, @NonNull String fileName) throws IOException {
122127
this(mode, new FileInputStream(file.toFile()), fileName, Files.size(file));
128+
this.file = file;
123129
}
124130

125131
/**
@@ -131,6 +137,25 @@ public FileDownload(Mode mode, @NonNull Path file, @NonNull String fileName) thr
131137
*/
132138
public FileDownload(Mode mode, @NonNull Path file) throws IOException {
133139
this(mode, file, file.getFileName().toString());
140+
this.file = file;
141+
}
142+
143+
/**
144+
* True if the file will be deleted after sending to the client.
145+
*
146+
* @return True if the file will be deleted after sending to the client.
147+
*/
148+
public boolean deleteOnComplete() {
149+
return deleteOnComplete;
150+
}
151+
152+
/**
153+
* Get the underlying file or <code>null</code>.
154+
*
155+
* @return Get the underlying file or <code>null</code>.
156+
*/
157+
public @Nullable Path getFile() {
158+
return file;
134159
}
135160

136161
/**
@@ -213,6 +238,21 @@ default FileDownload inline() {
213238
}
214239
}
215240

241+
/**
242+
* Add support for delete file on complete when created from {@link #build(Path, String)} or
243+
* {@link #build(Path)}.
244+
*/
245+
public interface BuilderExt extends Builder {
246+
247+
/**
248+
* Mark the file to be deleted once it send to the client. This options works on file created
249+
* within a {@link Path} instance.
250+
*
251+
* @return This builder.
252+
*/
253+
Builder deleteOnComplete();
254+
}
255+
216256
/**
217257
* Creates a builder with the specified content which can be used to create a {@link FileDownload}
218258
* with any {@link Mode}.
@@ -259,12 +299,25 @@ public static Builder build(@NonNull byte[] content, @NonNull String fileName) {
259299
* @param fileName Filename.
260300
* @return a {@link Builder} with the specified content
261301
*/
262-
public static Builder build(@NonNull Path file, @NonNull String fileName) {
263-
return mode -> {
264-
try {
265-
return new FileDownload(mode, file, fileName);
266-
} catch (IOException e) {
267-
throw SneakyThrows.propagate(e);
302+
public static BuilderExt build(@NonNull Path file, @NonNull String fileName) {
303+
return new BuilderExt() {
304+
private boolean deleteOnComplete;
305+
306+
@Override
307+
public FileDownload build(Mode mode) {
308+
try {
309+
var output = new FileDownload(mode, file, fileName);
310+
output.deleteOnComplete = deleteOnComplete;
311+
return output;
312+
} catch (IOException e) {
313+
throw SneakyThrows.propagate(e);
314+
}
315+
}
316+
317+
@Override
318+
public Builder deleteOnComplete() {
319+
this.deleteOnComplete = true;
320+
return this;
268321
}
269322
};
270323
}
@@ -276,13 +329,7 @@ public static Builder build(@NonNull Path file, @NonNull String fileName) {
276329
* @param file File content.
277330
* @return a {@link Builder} with the specified content
278331
*/
279-
public static Builder build(@NonNull Path file) {
280-
return mode -> {
281-
try {
282-
return new FileDownload(mode, file);
283-
} catch (IOException e) {
284-
throw SneakyThrows.propagate(e);
285-
}
286-
};
332+
public static BuilderExt build(@NonNull Path file) {
333+
return build(file, file.getFileName().toString());
287334
}
288335
}

modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.channels.FileChannel;
2222
import java.nio.channels.ReadableByteChannel;
2323
import java.nio.charset.Charset;
24+
import java.nio.file.Files;
2425
import java.security.cert.Certificate;
2526
import java.util.ArrayList;
2627
import java.util.Collection;
@@ -38,6 +39,7 @@
3839
import org.eclipse.jetty.http.HttpHeader;
3940
import org.eclipse.jetty.http.HttpHeaderValue;
4041
import org.eclipse.jetty.http.MimeTypes;
42+
import org.eclipse.jetty.io.ByteBufferPool;
4143
import org.eclipse.jetty.io.Content;
4244
import org.eclipse.jetty.io.content.InputStreamContentSource;
4345
import org.eclipse.jetty.server.Request;
@@ -51,39 +53,38 @@
5153

5254
import edu.umd.cs.findbugs.annotations.NonNull;
5355
import edu.umd.cs.findbugs.annotations.Nullable;
54-
import io.jooby.Body;
56+
import io.jooby.*;
5557
import io.jooby.ByteRange;
56-
import io.jooby.CompletionListeners;
57-
import io.jooby.Context;
58-
import io.jooby.Cookie;
59-
import io.jooby.DefaultContext;
60-
import io.jooby.FileUpload;
61-
import io.jooby.Formdata;
62-
import io.jooby.MediaType;
63-
import io.jooby.QueryString;
64-
import io.jooby.Route;
65-
import io.jooby.Router;
66-
import io.jooby.Sender;
67-
import io.jooby.Server;
68-
import io.jooby.ServerSentEmitter;
69-
import io.jooby.Session;
70-
import io.jooby.SessionStore;
71-
import io.jooby.SneakyThrows;
72-
import io.jooby.StatusCode;
73-
import io.jooby.WebSocket;
7458
import io.jooby.internal.jetty.http2.JettyHeaders;
7559
import io.jooby.output.Output;
7660
import io.jooby.value.Value;
7761

7862
public class JettyContext implements DefaultContext, Callback {
63+
private interface DeleteFileTask {
64+
void delete() throws IOException;
65+
66+
static DeleteFileTask of(FileUpload file) {
67+
return file::close;
68+
}
69+
70+
static DeleteFileTask of(FileDownload file) {
71+
return () -> {
72+
var path = file.getFile();
73+
if (path != null) {
74+
Files.delete(path);
75+
}
76+
};
77+
}
78+
}
79+
7980
private final int bufferSize;
8081
private final long maxRequestSize;
8182
Request request;
8283
Response response;
8384

8485
private QueryString query;
8586
private Formdata formdata;
86-
private List<FileUpload> files;
87+
private List<DeleteFileTask> files;
8788
private Value headers;
8889
private Map<String, String> pathMap = Collections.EMPTY_MAP;
8990
private Map<String, Object> attributes = new HashMap<>();
@@ -558,6 +559,14 @@ public Context send(@NonNull ReadableByteChannel channel) {
558559
return sendStreamInternal(Channels.newInputStream(channel));
559560
}
560561

562+
@Override
563+
public @NonNull Context send(@NonNull FileDownload file) {
564+
if (file.deleteOnComplete()) {
565+
register(DeleteFileTask.of(file));
566+
}
567+
return DefaultContext.super.send(file);
568+
}
569+
561570
@NonNull @Override
562571
public Context send(@NonNull InputStream in) {
563572
try {
@@ -582,7 +591,8 @@ private Context sendStreamInternal(@NonNull InputStream in) {
582591
stream = in;
583592
}
584593
responseStarted = true;
585-
Content.copy(new InputStreamContentSource(stream), response, this);
594+
Content.copy(
595+
new InputStreamContentSource(stream, ByteBufferPool.SIZED_NON_POOLING), response, this);
586596
return this;
587597
} catch (IOException x) {
588598
throw SneakyThrows.propagate(x);
@@ -633,11 +643,11 @@ public String toString() {
633643

634644
private void clearFiles() {
635645
if (files != null) {
636-
for (FileUpload file : files) {
646+
for (var file : files) {
637647
try {
638-
file.close();
648+
file.delete();
639649
} catch (Exception e) {
640-
router.getLog().debug("file upload destroy resulted in exception", e);
650+
router.getLog().debug("file destroy resulted in exception", e);
641651
}
642652
}
643653
files.clear();
@@ -708,11 +718,15 @@ private void ifSetChunked() {
708718
}
709719

710720
private FileUpload register(FileUpload upload) {
721+
register(DeleteFileTask.of(upload));
722+
return upload;
723+
}
724+
725+
private void register(DeleteFileTask deleteFileTask) {
711726
if (files == null) {
712727
files = new ArrayList<>();
713728
}
714-
files.add(upload);
715-
return upload;
729+
files.add(deleteFileTask);
716730
}
717731

718732
private static void formParam(Request request, Formdata form) {

0 commit comments

Comments
 (0)