Skip to content

Commit 38d338f

Browse files
authored
fetch: download specific file to adjacent temp (#629)
* get: also benefits from temp download mechanism * http: use HTTP/1.1 as default in options builder
1 parent 43204cd commit 38d338f

File tree

5 files changed

+17
-151
lines changed

5 files changed

+17
-151
lines changed

src/main/java/me/itzg/helpers/files/ReactiveFileUtils.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import java.nio.channels.FileChannel;
66
import java.nio.file.Files;
77
import java.nio.file.Path;
8+
import java.nio.file.StandardCopyOption;
89
import java.nio.file.StandardOpenOption;
910
import java.time.Instant;
11+
import java.util.function.Function;
1012
import java.util.stream.Collectors;
1113
import lombok.extern.slf4j.Slf4j;
1214
import reactor.core.publisher.Mono;
@@ -107,4 +109,13 @@ public static <T> Mono<T> removeFailedDownload(Throwable throwable, Path outputF
107109
})
108110
.subscribeOn(Schedulers.boundedElastic());
109111
}
112+
113+
public static Function<Path, Mono<Path>> moveTo(Path to) {
114+
return from -> Mono.fromCallable(() -> {
115+
log.debug("Moving {} to {}", from, to);
116+
Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);
117+
return to;
118+
})
119+
.subscribeOn(Schedulers.boundedElastic());
120+
}
110121
}

src/main/java/me/itzg/helpers/get/GetCommand.java

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,6 @@ public class GetCommand implements Callable<Integer> {
134134
@Option(names = "--retry-delay", description = "in seconds", defaultValue = "2")
135135
int retryDelay;
136136

137-
@Option(names = {"--use-temp-file"},
138-
description = "Download to a temporary file in the same directory with .download extension, then rename to the final destination when complete",
139-
defaultValue = "false")
140-
boolean useTempFile;
141-
142137
@Parameters(split = OPTION_SPLIT_COMMAS, paramLabel = "URI",
143138
description = "The URI of the resource to retrieve. When the output is a directory,"
144139
+ " more than one URI can be requested.",
@@ -214,10 +209,8 @@ null, new JsonPathOutputHandler(
214209
stdout.println(outputFile);
215210
}
216211
} else {
217-
final Path saveToFile = getSaveToFile();
218-
try {
219212
final Path file = fetch(uris.get(0))
220-
.toFile(saveToFile)
213+
.toFile(outputFile)
221214
.skipUpToDate(skipUpToDate)
222215
.acceptContentTypes(acceptContentTypes)
223216
.handleDownloaded((uri, f, contentSizeBytes) -> {
@@ -226,25 +219,9 @@ null, new JsonPathOutputHandler(
226219
}
227220
})
228221
.execute();
229-
if (useTempFile) {
230-
Files.move(saveToFile, outputFile);
231-
}
232222
if (this.outputFilename) {
233223
stdout.println(file);
234224
}
235-
} catch (Exception e) {
236-
// Clean up temp file if download fails
237-
if (useTempFile && Files.exists(saveToFile)) {
238-
try {
239-
Files.delete(saveToFile);
240-
log.debug("Cleaned up temporary file {} after failed download", saveToFile);
241-
} catch (IOException cleanupEx) {
242-
log.warn("Failed to clean up temporary file {} after failed download",
243-
saveToFile, cleanupEx);
244-
}
245-
}
246-
throw e; // Re-throw the original exception
247-
}
248225
}
249226
}
250227
} catch (URISyntaxException e) {
@@ -500,10 +477,5 @@ private static URI removeUserInfo(URI uri) throws URISyntaxException {
500477
);
501478
}
502479

503-
private Path getSaveToFile() {
504-
return useTempFile ?
505-
Paths.get(outputFile.toString() + ".download") :
506-
outputFile;
507-
}
508480

509481
}

src/main/java/me/itzg/helpers/http/SharedFetch.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ public static class Options {
163163
private final URI filesViaUrl;
164164

165165
@Default
166-
private final boolean useHttp2 = true;
166+
private final boolean useHttp2 = false;
167167

168168
private final boolean wiretap;
169169

src/main/java/me/itzg/helpers/http/SpecificFileFetchBuilder.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public Mono<Path> assemble() {
6464

6565
final boolean useIfModifiedSince = skipUpToDate && Files.exists(file);
6666

67+
final Path tempDownloadFile = file.getParent().resolve(file.getFileName() + ".download");
6768
return useReactiveClient(client ->
6869
client
6970
.doOnRequest((httpClientRequest, connection) ->
@@ -107,7 +108,7 @@ public Mono<Path> assemble() {
107108
return failedContentTypeMono(resp);
108109
}
109110

110-
return ReactiveFileUtils.writeByteBufFluxToFile(byteBufFlux, file)
111+
return ReactiveFileUtils.writeByteBufFluxToFile(byteBufFlux, tempDownloadFile)
111112
.flatMap(fileSize -> {
112113
statusHandler.call(FileDownloadStatus.DOWNLOADED, uri, file);
113114
downloadedHandler.call(uri, file, fileSize);
@@ -120,12 +121,13 @@ public Mono<Path> assemble() {
120121
uri, formatDuration(durationMillis), transferRate(durationMillis, fileSize)
121122
);
122123
}
123-
return Mono.just(file);
124+
return Mono.just(tempDownloadFile);
124125
});
125126
});
126127

127128
})
128129
.last()
130+
.flatMap(ReactiveFileUtils.moveTo(file))
129131
.contextWrite(context -> context.put("downloadStart", currentTimeMillis()))
130132
);
131133
}

src/test/java/me/itzg/helpers/get/OutputToFileTest.java

Lines changed: 0 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -230,123 +230,4 @@ void skipsUpToDate_butDownloadsWhenAbsent(@TempDir Path tempDir) throws IOExcept
230230
assertThat(fileToDownload).hasContent("New content");
231231
}
232232

233-
@Test
234-
void successfulWithTemporaryFile(@TempDir Path tempDir) throws MalformedURLException, IOException {
235-
mock.expectRequest("GET", "/downloadsToFile.txt",
236-
response()
237-
.withBody("Response content to file", MediaType.TEXT_PLAIN)
238-
);
239-
240-
final Path expectedFile = tempDir.resolve("out.txt");
241-
242-
final int status =
243-
new CommandLine(new GetCommand())
244-
.execute(
245-
"-o",
246-
expectedFile.toString(),
247-
"--use-temp-file",
248-
mock.buildMockedUrl("/downloadsToFile.txt").toString()
249-
);
250-
251-
assertThat(status).isEqualTo(0);
252-
assertThat(expectedFile).exists();
253-
assertThat(expectedFile).hasContent("Response content to file");
254-
// The temporary file with .download extension should no longer exist after successful download
255-
assertThat(tempDir.resolve("out.txt.download")).doesNotExist();
256-
}
257-
258-
@Test
259-
void handlesExistingDownloadFile(@TempDir Path tempDir) throws MalformedURLException, IOException {
260-
mock.expectRequest("GET", "/downloadsToFile.txt",
261-
response()
262-
.withBody("New content", MediaType.TEXT_PLAIN)
263-
);
264-
265-
final Path expectedFile = tempDir.resolve("out.txt");
266-
final Path downloadFile = tempDir.resolve("out.txt.download");
267-
268-
// Create a pre-existing .download file with different content
269-
Files.writeString(downloadFile, "Partial old content");
270-
271-
final int status =
272-
new CommandLine(new GetCommand())
273-
.execute(
274-
"-o",
275-
expectedFile.toString(),
276-
"--use-temp-file",
277-
mock.buildMockedUrl("/downloadsToFile.txt").toString()
278-
);
279-
280-
assertThat(status).isEqualTo(0);
281-
assertThat(expectedFile).exists();
282-
assertThat(expectedFile).hasContent("New content");
283-
// The temporary file should be gone
284-
assertThat(downloadFile).doesNotExist();
285-
}
286-
287-
@Test
288-
void preservesOriginalWhenErrorOccurs(@TempDir Path tempDir) throws MalformedURLException, IOException {
289-
mock.expectRequest("GET", "/errorFile.txt",
290-
response()
291-
.withStatusCode(500)
292-
.withBody("Server error", MediaType.TEXT_PLAIN)
293-
);
294-
295-
final Path expectedFile = tempDir.resolve("out.txt");
296-
final String originalContent = "Original content that should be preserved";
297-
298-
// Create the original file with content that should remain untouched
299-
Files.writeString(expectedFile, originalContent);
300-
301-
final int status =
302-
new CommandLine(new GetCommand())
303-
.execute(
304-
"-o",
305-
expectedFile.toString(),
306-
"--use-temp-file",
307-
mock.buildMockedUrl("/errorFile.txt").toString()
308-
);
309-
310-
// Should fail with non-zero status
311-
assertThat(status).isNotEqualTo(0);
312-
// Original file should still exist with unchanged content
313-
assertThat(expectedFile).exists();
314-
assertThat(expectedFile).hasContent(originalContent);
315-
// Any temporary download file should be cleaned up
316-
assertThat(tempDir.resolve("out.txt.download")).doesNotExist();
317-
}
318-
319-
@Test
320-
void preservesOriginalWhenDownloadHasInvalidContent(@TempDir Path tempDir) throws MalformedURLException, IOException {
321-
// Set up a request that will result in an error during processing
322-
// We'll return content with a valid Content-Length but corrupted/truncated data
323-
mock.expectRequest("GET", "/interruptedFile.txt",
324-
response()
325-
.withHeader("Content-Length", "1000") // Much larger than actual content
326-
.withBody("This is only part of the expected content...") // Truncated content
327-
);
328-
329-
final Path expectedFile = tempDir.resolve("out.txt");
330-
final String originalContent = "Original content that should remain intact";
331-
332-
// Create the original file with content that should remain untouched
333-
Files.writeString(expectedFile, originalContent);
334-
335-
final int status =
336-
new CommandLine(new GetCommand())
337-
.execute(
338-
"-o",
339-
expectedFile.toString(),
340-
"--use-temp-file",
341-
mock.buildMockedUrl("/interruptedFile.txt").toString()
342-
);
343-
344-
// Original file should still exist with unchanged content
345-
assertThat(expectedFile).exists();
346-
assertThat(expectedFile).hasContent(originalContent);
347-
348-
// Any temporary download file should be cleaned up
349-
assertThat(tempDir.resolve("out.txt.download")).doesNotExist();
350-
}
351-
352233
}

0 commit comments

Comments
 (0)