diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java b/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java index 44f952b565a..8f5f0df1ce9 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java @@ -213,8 +213,18 @@ public void setTemporaryFileSuffix(String temporaryFileSuffix) { *

* Otherwise the LockRegistry is set to {@link PassThruLockRegistry} which * has no effect. + *

+ * With {@link FileExistsMode#REPLACE_IF_MODIFIED}, if the file exists, + * it is only replaced if its last modified timestamp is different to the + * source; otherwise, the write is ignored. For {@link File} payloads, + * the actual timestamp of the {@link File} is compared; for other payloads, + * the {@link FileHeaders#SET_MODIFIED} is compared to the existing file. + * If the header is missing, or its value is not a {@link Number}, the file + * is always replaced. This mode will typically only make sense if + * {@link #setPreserveTimestamp(boolean) preserveTimestamp} is true. * * @param fileExistsMode Must not be null + * @see #setPreserveTimestamp(boolean) */ public void setFileExistsMode(FileExistsMode fileExistsMode) { @@ -426,26 +436,30 @@ protected Object handleRequestMessage(Message requestMessage) { File tempFile = new File(destinationDirectoryToUse, generatedFileName + this.temporaryFileSuffix); File resultFile = new File(destinationDirectoryToUse, generatedFileName); - if (FileExistsMode.FAIL.equals(this.fileExistsMode) && resultFile.exists()) { + boolean exists = resultFile.exists(); + if (exists && FileExistsMode.FAIL.equals(this.fileExistsMode)) { throw new MessageHandlingException(requestMessage, "The destination file already exists at '" + resultFile.getAbsolutePath() + "'."); } - final boolean ignore = FileExistsMode.IGNORE.equals(this.fileExistsMode) && - (resultFile.exists() || - (StringUtils.hasText(this.temporaryFileSuffix) && tempFile.exists())); - + Object timestamp = requestMessage.getHeaders().get(FileHeaders.SET_MODIFIED); + if (payload instanceof File) { + timestamp = ((File) payload).lastModified(); + } + boolean ignore = (FileExistsMode.IGNORE.equals(this.fileExistsMode) + && (exists || (StringUtils.hasText(this.temporaryFileSuffix) && tempFile.exists()))) + || ((exists && FileExistsMode.REPLACE_IF_MODIFIED.equals(this.fileExistsMode)) + && (timestamp instanceof Number + && ((Number) timestamp).longValue() == resultFile.lastModified())); if (!ignore) { try { - Object timestamp = requestMessage.getHeaders().get(FileHeaders.SET_MODIFIED); - if (!resultFile.exists() && + if (!exists && generatedFileName.replaceAll("/", Matcher.quoteReplacement(File.separator)) .contains(File.separator)) { resultFile.getParentFile().mkdirs(); //NOSONAR - will fail on the writing below } if (payload instanceof File) { resultFile = handleFileMessage((File) payload, tempFile, resultFile); - timestamp = ((File) payload).lastModified(); } else if (payload instanceof InputStream) { resultFile = handleInputStreamMessage((InputStream) payload, originalFileFromHeader, tempFile, @@ -711,6 +725,7 @@ private File determineFileToWrite(File resultFile, File tempFile) { case FAIL: case IGNORE: case REPLACE: + case REPLACE_IF_MODIFIED: fileToWriteTo = tempFile; break; default: diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java index dae33151155..dcc0a12cba2 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -282,6 +282,8 @@ private String send(final Message message, final String subDirectory, final F Assert.notNull(this.directoryExpressionProcessor, "'remoteDirectoryExpression' is required"); Assert.isTrue(!FileExistsMode.APPEND.equals(mode) || !this.useTemporaryFileName, "Cannot append when using a temporary file name"); + Assert.isTrue(!FileExistsMode.REPLACE_IF_MODIFIED.equals(mode), + "FilExistsMode.REPLACE_IF_MODIFIED can only be used for local files"); final StreamHolder inputStreamHolder = this.payloadToInputStream(message); if (inputStreamHolder != null) { try { diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java index 9911a102280..27395fef72d 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java @@ -587,7 +587,7 @@ private Object doGet(final Message requestMessage) { } else { payload = this.remoteFileTemplate.execute(session1 -> - get(requestMessage, session1, remoteDir, remoteFilePath, remoteFilename, true)); + get(requestMessage, session1, remoteDir, remoteFilePath, remoteFilename, null)); } return getMessageBuilderFactory().withPayload(payload) .setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir) @@ -831,33 +831,38 @@ protected void purgeDots(List lsFiles) { * Copy a remote file to the configured local directory. * * - * @param message The message. - * @param session The session. - * @param remoteDir The remote directory. - * @param remoteFilePath The remote file path. - * @param remoteFilename The remote file name. - * @param lsFirst true to execute an 'ls' command first. + * @param message the message. + * @param session the session. + * @param remoteDir the remote directory. + * @param remoteFilePath the remote file path. + * @param remoteFilename the remote file name. + * @param fileInfoParam the remote file info; if null we will execute an 'ls' command + * first. * @return The file. * @throws IOException Any IOException. */ protected File get(Message message, Session session, String remoteDir, String remoteFilePath, - String remoteFilename, boolean lsFirst) throws IOException { - F[] files = null; - if (lsFirst) { - files = session.list(remoteFilePath); + String remoteFilename, F fileInfoParam) throws IOException { + F fileInfo = fileInfoParam; + if (fileInfo == null) { + F[] files = session.list(remoteFilePath); if (files == null) { throw new MessagingException("Session returned null when listing " + remoteFilePath); } - if (files.length != 1 || isDirectory(files[0]) || isLink(files[0])) { + if (files.length != 1 || files[0] == null || isDirectory(files[0]) || isLink(files[0])) { throw new MessagingException(remoteFilePath + " is not a file"); } + fileInfo = files[0]; } File localFile = new File(generateLocalDirectory(message, remoteDir), generateLocalFileName(message, remoteFilename)); FileExistsMode fileExistsMode = this.fileExistsMode; boolean appending = FileExistsMode.APPEND.equals(fileExistsMode); - boolean replacing = FileExistsMode.REPLACE.equals(fileExistsMode); - if (!localFile.exists() || appending || replacing) { + boolean exists = localFile.exists(); + boolean replacing = FileExistsMode.REPLACE.equals(fileExistsMode) + || (exists && FileExistsMode.REPLACE_IF_MODIFIED.equals(fileExistsMode) + && localFile.lastModified() != getModified(fileInfo)); + if (!exists || appending || replacing) { OutputStream outputStream; String tempFileName = localFile.getAbsolutePath() + this.remoteFileTemplate.getTemporaryFileSuffix(); File tempFile = new File(tempFileName); @@ -898,11 +903,14 @@ protected File get(Message message, Session session, String remoteDir, Str if (!appending && !tempFile.renameTo(localFile)) { throw new MessagingException("Failed to rename local file"); } - if (lsFirst && this.options.contains(Option.PRESERVE_TIMESTAMP)) { - localFile.setLastModified(getModified(files[0])); + if (this.options.contains(Option.PRESERVE_TIMESTAMP)) { + localFile.setLastModified(getModified(fileInfo)); } } - else if (FileExistsMode.IGNORE != fileExistsMode) { + else if (FileExistsMode.REPLACE_IF_MODIFIED.equals(fileExistsMode)) { + logger.debug("Local file '" + localFile + "' has the same modified timestamp, ignored"); + } + else if (!FileExistsMode.IGNORE.equals(fileExistsMode)) { throw new MessageHandlingException(message, "Local file " + localFile + " already exists"); } else { @@ -955,10 +963,7 @@ private List mGetWithoutRecursion(Message message, Session session, String fileName = this.getRemoteFilename(fullFileName); String actualRemoteDirectory = this.getRemoteDirectory(fullFileName, fileName); File file = get(message, session, actualRemoteDirectory, - fullFileName, fileName, false); - if (this.options.contains(Option.PRESERVE_TIMESTAMP)) { - file.setLastModified(getModified(lsEntry.getFileInfo())); - } + fullFileName, fileName, lsEntry.getFileInfo()); files.add(file); } } @@ -1001,10 +1006,7 @@ private List mGetWithRecursion(Message message, Session session, Str String fileName = this.getRemoteFilename(fullFileName); String actualRemoteDirectory = this.getRemoteDirectory(fullFileName, fileName); File file = get(message, session, actualRemoteDirectory, - fullFileName, fileName, false); - if (this.options.contains(Option.PRESERVE_TIMESTAMP)) { - file.setLastModified(getModified(lsEntry.getFileInfo())); - } + fullFileName, fileName, lsEntry.getFileInfo()); files.add(file); } } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileExistsMode.java b/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileExistsMode.java index 9271bfc0478..86574e2903d 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileExistsMode.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileExistsMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,14 @@ public enum FileExistsMode { /** * If the file already exists, replace it. */ - REPLACE; + REPLACE, + + /** + * If the file already exists, replace it only if the last modified time + * is different. Only applies to local files. + * @since 5.0 + */ + REPLACE_IF_MODIFIED; /** * For a given non-null and not-empty input string, this method returns the diff --git a/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-5.0.xsd b/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-5.0.xsd index 827e0673410..db6db2891bd 100644 --- a/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-5.0.xsd +++ b/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-5.0.xsd @@ -828,6 +828,15 @@ Only files matching this regular expression will be picked up by this adapter. ]]> + + + + + diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java index 3bbc966d726..c10a2082619 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java @@ -518,6 +518,61 @@ public void noFlushAppend() throws Exception { handler.stop(); } + @Test + public void replaceIfDifferent() throws IOException { + QueueChannel output = new QueueChannel(); + this.handler.setOutputChannel(output); + this.handler.setPreserveTimestamp(true); + this.handler.setFileExistsMode(FileExistsMode.REPLACE_IF_MODIFIED); + this.handler.handleMessage(MessageBuilder.withPayload("foo") + .setHeader(FileHeaders.FILENAME, "replaceIfDifferent.txt") + .setHeader(FileHeaders.SET_MODIFIED, 42_000_000) + .build()); + Message result = output.receive(0); + assertFileContentIs(result, "foo"); + assertLastModifiedIs(result, 42_000_000); + this.handler.handleMessage(MessageBuilder.withPayload("bar") + .setHeader(FileHeaders.FILENAME, "replaceIfDifferent.txt") + .setHeader(FileHeaders.SET_MODIFIED, 42_000_000) + .build()); + result = output.receive(0); + assertFileContentIs(result, "foo"); // no overwrite - timestamp same + assertLastModifiedIs(result, 42_000_000); + this.handler.handleMessage(MessageBuilder.withPayload("bar") + .setHeader(FileHeaders.FILENAME, "replaceIfDifferent.txt") + .setHeader(FileHeaders.SET_MODIFIED, 43_000_000) + .build()); + result = output.receive(0); + assertFileContentIs(result, "bar"); + assertLastModifiedIs(result, 43_000_000); + } + + @Test + public void replaceIfDifferentFile() throws IOException { + File file = new File(this.temp.newFolder(), "foo.txt"); + FileCopyUtils.copy("foo".getBytes(), new FileOutputStream(file)); + file.setLastModified(42_000_000); + QueueChannel output = new QueueChannel(); + this.handler.setOutputChannel(output); + this.handler.setPreserveTimestamp(true); + this.handler.setFileExistsMode(FileExistsMode.REPLACE_IF_MODIFIED); + this.handler.handleMessage(MessageBuilder.withPayload(file).build()); + Message result = output.receive(0); + assertFileContentIs(result, "foo"); + assertLastModifiedIs(result, 42_000_000); + FileCopyUtils.copy("bar".getBytes(), new FileOutputStream(file)); + file.setLastModified(42_000_000); + this.handler.handleMessage(MessageBuilder.withPayload(file).build()); + result = output.receive(0); + assertFileContentIs(result, "foo"); // no overwrite - timestamp same + assertLastModifiedIs(result, 42_000_000); + file.setLastModified(43_000_000); + this.handler.handleMessage(MessageBuilder.withPayload(file).build()); + result = output.receive(0); + assertFileContentIs(result, "bar"); + assertLastModifiedIs(result, 43_000_000); + } + void assertFileContentIsMatching(Message result) throws IOException { assertFileContentIs(result, SAMPLE_CONTENT); } diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml index 78f0e0d3dd2..8c05f1a78c4 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml @@ -53,6 +53,7 @@ command="mget" expression="payload" command-options="-R -P" + mode="REPLACE_IF_MODIFIED" filter="starDotTxtFilter" local-directory-expression="@extraConfig.targetLocalDirectoryName + #remoteDirectory" local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')" diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java index 47cd566e1b2..9eb1b7c9729 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java @@ -36,6 +36,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -48,6 +49,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.io.FileUtils; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPFile; import org.hamcrest.Matchers; @@ -247,9 +249,11 @@ public void testMGETOnNullDir() throws IOException { @Test @SuppressWarnings("unchecked") - public void testInt3172LocalDirectoryExpressionMGETRecursive() { + public void testInt3172LocalDirectoryExpressionMGETRecursive() throws IOException { String dir = "ftpSource/"; long modified = setModifiedOnSource1(); + File secondRemote = new File(getSourceRemoteDirectory(), "ftpSource2.txt"); + secondRemote.setLastModified(System.currentTimeMillis() - 1_000_000); this.inboundMGetRecursive.send(new GenericMessage("*")); Message result = this.output.receive(1000); assertNotNull(result); @@ -268,6 +272,30 @@ public void testInt3172LocalDirectoryExpressionMGETRecursive() { assertThat(localFiles.get(2).getPath().replaceAll(quoteReplacement(File.separator), "/"), containsString(dir + "subFtpSource")); + File secondTarget = new File(getTargetLocalDirectory() + File.separator + "ftpSource", "localTarget2.txt"); + ByteArrayOutputStream remoteContents = new ByteArrayOutputStream(); + ByteArrayOutputStream localContents = new ByteArrayOutputStream(); + FileUtils.copyFile(secondRemote, remoteContents); + FileUtils.copyFile(secondTarget, localContents); + String localAsString = new String(localContents.toByteArray()); + assertEquals(new String(remoteContents.toByteArray()), localAsString); + long oldLastModified = secondRemote.lastModified(); + FileUtils.copyInputStreamToFile(new ByteArrayInputStream("junk".getBytes()), secondRemote); + long newLastModified = secondRemote.lastModified(); + secondRemote.setLastModified(oldLastModified); + this.inboundMGetRecursive.send(new GenericMessage("*")); + this.output.receive(0); + localContents = new ByteArrayOutputStream(); + FileUtils.copyFile(secondTarget, localContents); + assertEquals(localAsString, new String(localContents.toByteArray())); + secondRemote.setLastModified(newLastModified); + this.inboundMGetRecursive.send(new GenericMessage("*")); + this.output.receive(0); + localContents = new ByteArrayOutputStream(); + FileUtils.copyFile(secondTarget, localContents); + assertEquals("junk", new String(localContents.toByteArray())); + // restore the remote file contents + FileUtils.copyInputStreamToFile(new ByteArrayInputStream(localAsString.getBytes()), secondRemote); } private long setModifiedOnSource1() { diff --git a/spring-integration-gemfire/src/test/resources/log4j.xml b/spring-integration-gemfire/src/test/resources/log4j.xml index d99732eeb1d..e871f822167 100644 --- a/spring-integration-gemfire/src/test/resources/log4j.xml +++ b/spring-integration-gemfire/src/test/resources/log4j.xml @@ -25,4 +25,4 @@ - \ No newline at end of file + diff --git a/spring-integration-scripting/src/test/resources/log4j.xml b/spring-integration-scripting/src/test/resources/log4j.xml index 575d062f62f..41d1078621b 100644 --- a/spring-integration-scripting/src/test/resources/log4j.xml +++ b/spring-integration-scripting/src/test/resources/log4j.xml @@ -26,4 +26,4 @@ - \ No newline at end of file + diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml index 0738f2f8768..79b78910d24 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml @@ -53,6 +53,7 @@ command="mget" expression="payload" command-options="-R -P" + mode="REPLACE_IF_MODIFIED" filter="dotStarDotTxtFilter" local-directory-expression="@extraConfig.targetLocalDirectoryName + #remoteDirectory" local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')" diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java index df83d5227d2..691ee21523f 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -40,6 +41,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.FileUtils; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; @@ -208,9 +210,11 @@ public void testInt2866LocalDirectoryExpressionMGET() { @Test @SuppressWarnings("unchecked") - public void testInt3172LocalDirectoryExpressionMGETRecursive() { + public void testInt3172LocalDirectoryExpressionMGETRecursive() throws IOException { String dir = "sftpSource/"; long modified = setModifiedOnSource1(); + File secondRemote = new File(getSourceRemoteDirectory(), "sftpSource2.txt"); + secondRemote.setLastModified(System.currentTimeMillis() - 1_000_000); this.inboundMGetRecursive.send(new GenericMessage(dir + "*")); Message result = this.output.receive(1000); assertNotNull(result); @@ -229,6 +233,30 @@ public void testInt3172LocalDirectoryExpressionMGETRecursive() { assertThat(localFiles.get(2).getPath().replaceAll(quoteReplacement(File.separator), "/"), containsString(dir + "subSftpSource")); + File secondTarget = new File(getTargetLocalDirectory() + File.separator + "sftpSource", "localTarget2.txt"); + ByteArrayOutputStream remoteContents = new ByteArrayOutputStream(); + ByteArrayOutputStream localContents = new ByteArrayOutputStream(); + FileUtils.copyFile(secondRemote, remoteContents); + FileUtils.copyFile(secondTarget, localContents); + String localAsString = new String(localContents.toByteArray()); + assertEquals(new String(remoteContents.toByteArray()), localAsString); + long oldLastModified = secondRemote.lastModified(); + FileUtils.copyInputStreamToFile(new ByteArrayInputStream("junk".getBytes()), secondRemote); + long newLastModified = secondRemote.lastModified(); + secondRemote.setLastModified(oldLastModified); + this.inboundMGetRecursive.send(new GenericMessage(dir + "*")); + this.output.receive(0); + localContents = new ByteArrayOutputStream(); + FileUtils.copyFile(secondTarget, localContents); + assertEquals(localAsString, new String(localContents.toByteArray())); + secondRemote.setLastModified(newLastModified); + this.inboundMGetRecursive.send(new GenericMessage(dir + "*")); + this.output.receive(0); + localContents = new ByteArrayOutputStream(); + FileUtils.copyFile(secondTarget, localContents); + assertEquals("junk", new String(localContents.toByteArray())); + // restore the remote file contents + FileUtils.copyInputStreamToFile(new ByteArrayInputStream(localAsString.getBytes()), secondRemote); } private long setModifiedOnSource1() { diff --git a/src/reference/asciidoc/file.adoc b/src/reference/asciidoc/file.adoc index 74a3ee4d204..95b83f9cd49 100644 --- a/src/reference/asciidoc/file.adoc +++ b/src/reference/asciidoc/file.adoc @@ -617,6 +617,7 @@ This behavior, though, can be changed by setting the _mode_ attribute on the res The following options exist: * REPLACE (Default) +* REPLACE_IF_MODIFIED * APPEND * APPEND_NO_FLUSH * FAIL @@ -631,6 +632,13 @@ _REPLACE_ If the target file already exists, it will be overwritten. If the _mode_ attribute is not specified, then this is the default behavior when writing files. +_REPLACE_IF_MODIFIED_ + +If the target file already exists, it will be overwritten only if the last modified timestamp is different to the source file. +For `File` payloads, the payload `lastModified` time is compared to the existing file. +For other payloads, the `FileHeaders.SET_MODIFIED` (`file_setModified`) header is compared to the existing file. +If the header is missing, or has a value that is not a `Number`, the file is always replaced. + _APPEND_ This mode allows you to append Message content to the existing file instead of creating a new file each time. diff --git a/src/reference/asciidoc/ftp.adoc b/src/reference/asciidoc/ftp.adoc index fa7646f2c66..6636b07dec7 100644 --- a/src/reference/asciidoc/ftp.adoc +++ b/src/reference/asciidoc/ftp.adoc @@ -848,12 +848,15 @@ _mget_ retrieves multiple remote files based on a pattern and supports the follo * -P - preserve the timestamps of the remote files +* -R - retrieve the entire directory tree recursively * -x - Throw an exception if no files match the pattern (otherwise an empty list is returned) The message payload resulting from an _mget_ operation is a `List` object - a List of File objects, each representing a retrieved file. -The remote directory is provided in the `file_remoteDirectory` header, and the pattern for the file names is provided in the `file_remoteFile` header. +The expression used to determine the remote path should produce a result that ends with `*` - e.g. `foo/*` will fetch the complete tree under `foo`. + +Starting with _version 5.0_, a recursive MGET, combined with the new `FileExistsMode.REPLACE_IF_MODIFIED` mode, can be used to periodically synchronize an entire remote directory tree locally. [NOTE] .Notes for when using recursion (`-R`) diff --git a/src/reference/asciidoc/mongodb.adoc b/src/reference/asciidoc/mongodb.adoc index 5ab8537fdf2..b4ca7a0cdbc 100644 --- a/src/reference/asciidoc/mongodb.adoc +++ b/src/reference/asciidoc/mongodb.adoc @@ -410,4 +410,4 @@ private MongoDbOutboundGatewaySpec collectionCallbackOutboundGateway() { .collectionCallback(MongoCollection::count) .collectionName("foo"); } ----- \ No newline at end of file +---- diff --git a/src/reference/asciidoc/security.adoc b/src/reference/asciidoc/security.adoc index 4f54d3b014e..fb178ef80d4 100644 --- a/src/reference/asciidoc/security.adoc +++ b/src/reference/asciidoc/security.adoc @@ -183,4 +183,4 @@ public interface SecuredGateway { Future send(String payload); } ----- \ No newline at end of file +---- diff --git a/src/reference/asciidoc/sftp.adoc b/src/reference/asciidoc/sftp.adoc index a35ac7b4db4..ab049f5a9ca 100644 --- a/src/reference/asciidoc/sftp.adoc +++ b/src/reference/asciidoc/sftp.adoc @@ -866,12 +866,15 @@ _mget_ retrieves multiple remote files based on a pattern and supports the follo * -P - preserve the timestamps of the remote files +* -R - retrieve the entire directory tree recursively * -x - Throw an exception if no files match the pattern (otherwise an empty list is returned) The message payload resulting from an _mget_ operation is a `List` object - a List of File objects, each representing a retrieved file. -The remote directory is provided in the `file_remoteDirectory` header, and the pattern for the filenames is provided in the `file_remoteFile` header. +The expression used to determine the remote path should produce a result that ends with `*` - e.g. `foo/*` will fetch the complete tree under `foo`. + +Starting with _version 5.0_, a recursive MGET, combined with the new `FileExistsMode.REPLACE_IF_MODIFIED` mode, can be used to periodically synchronize an entire remote directory tree locally. [NOTE] .Notes for when using recursion (`-R`) diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 19979cae38e..8554f7f0412 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -69,6 +69,8 @@ See <> for more information. The flush predicates for the `FileWritingMessageHandler` now have an additional parameter. See <> for more information. +The file outbound channel adapter (`FileWritingMessageHandler`) now supports the `REPLACE_IF_MODIFIED` `FileExistsMode`. + ==== (S)FTP Changes The inbound channel adapters now have a property `max-fetch-size` which is used to limit the number of files fetched during a poll when there are no files currently in the local directory. @@ -77,6 +79,9 @@ The regex and pattern filters can now be configured to always pass directories. This can be useful when using recursion in the outbound gateways. See <> and <> for more information. +The FTP and SFTP outbound gateways now support the `REPLACE_IF_MODIFIED` `FileExistsMode` when fetching remote files. +See <> and <> for more information. + ==== Integration Properties Since _version 4.3.2_ a new `spring.integration.readOnly.headers` global property has been added to customize the list of headers which should not be copied to a newly created `Message` by the `MessageBuilder`.