diff --git a/build.gradle b/build.gradle index 5b06609a7..4e02142e2 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,12 @@ license { mapping { java = 'SLASHSTAR_STYLE' } - excludes(['**/djb/Curve25519.java', '**/sshj/common/Base64.java', '**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java']) + excludes([ + '**/djb/Curve25519.java', + '**/sshj/common/Base64.java', + '**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java', + '**/files/test_file_*.txt', + ]) } if (!JavaVersion.current().isJava9Compatible()) { diff --git a/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java b/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java index 8075377e8..6a5494343 100644 --- a/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java +++ b/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java @@ -261,7 +261,16 @@ public int getLength() { private boolean eof; public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) { - this(maxUnconfirmedReads, 0L, -1L); + this(maxUnconfirmedReads, 0L); + } + + /** + * + * @param maxUnconfirmedReads Maximum number of unconfirmed requests to send + * @param fileOffset Initial offset in file to read from + */ + public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) { + this(maxUnconfirmedReads, fileOffset, -1L); } /** diff --git a/src/main/java/net/schmizz/sshj/sftp/SFTPClient.java b/src/main/java/net/schmizz/sshj/sftp/SFTPClient.java index c67009dc2..2ea5c3337 100644 --- a/src/main/java/net/schmizz/sshj/sftp/SFTPClient.java +++ b/src/main/java/net/schmizz/sshj/sftp/SFTPClient.java @@ -232,21 +232,41 @@ public void get(String source, String dest) throws IOException { xfer.download(source, dest); } + + public void get(String source, String dest, long byteOffset) + throws IOException { + xfer.download(source, dest, byteOffset); + } public void put(String source, String dest) throws IOException { xfer.upload(source, dest); } + public void put(String source, String dest, long byteOffset) + throws IOException { + xfer.upload(source, dest, byteOffset); + } + public void get(String source, LocalDestFile dest) throws IOException { xfer.download(source, dest); } + + public void get(String source, LocalDestFile dest, long byteOffset) + throws IOException { + xfer.download(source, dest, byteOffset); + } public void put(LocalSourceFile source, String dest) throws IOException { xfer.upload(source, dest); } + + public void put(LocalSourceFile source, String dest, long byteOffset) + throws IOException { + xfer.upload(source, dest, byteOffset); + } @Override public void close() diff --git a/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java b/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java index 6a260d2e2..bef6c694e 100644 --- a/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java +++ b/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java @@ -50,25 +50,47 @@ public void setPreserveAttributes(boolean preserveAttributes) { @Override public void upload(String source, String dest) throws IOException { - upload(new FileSystemFile(source), dest); + upload(source, dest, 0); + } + + @Override + public void upload(String source, String dest, long byteOffset) + throws IOException { + upload(new FileSystemFile(source), dest, byteOffset); } @Override public void download(String source, String dest) throws IOException { - download(source, new FileSystemFile(dest)); + download(source, dest, 0); + } + + @Override + public void download(String source, String dest, long byteOffset) + throws IOException { + download(source, new FileSystemFile(dest), byteOffset); } @Override public void upload(LocalSourceFile localFile, String remotePath) throws IOException { - new Uploader(localFile, remotePath).upload(getTransferListener()); + upload(localFile, remotePath, 0); + } + + @Override + public void upload(LocalSourceFile localFile, String remotePath, long byteOffset) throws IOException { + new Uploader(localFile, remotePath).upload(getTransferListener(), byteOffset); } @Override public void download(String source, LocalDestFile dest) throws IOException { + download(source, dest, 0); + } + + @Override + public void download(String source, LocalDestFile dest, long byteOffset) throws IOException { final PathComponents pathComponents = engine.getPathHelper().getComponents(source); final FileAttributes attributes = engine.stat(source); - new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest); + new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest, byteOffset); } public void setUploadFilter(LocalFileFilter uploadFilter) { @@ -92,7 +114,8 @@ private class Downloader { @SuppressWarnings("PMD.MissingBreakInSwitch") private void download(final TransferListener listener, final RemoteResourceInfo remote, - final LocalDestFile local) throws IOException { + final LocalDestFile local, + final long byteOffset) throws IOException { final LocalDestFile adjustedFile; switch (remote.getAttributes().getType()) { case DIRECTORY: @@ -101,8 +124,9 @@ private void download(final TransferListener listener, case UNKNOWN: log.warn("Server did not supply information about the type of file at `{}` " + "-- assuming it is a regular file!", remote.getPath()); + // fall through case REGULAR: - adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local); + adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local, byteOffset); break; default: throw new IOException(remote + " is not a regular file or directory"); @@ -119,7 +143,7 @@ private LocalDestFile downloadDir(final TransferListener listener, final RemoteDirectory rd = engine.openDir(remote.getPath()); try { for (RemoteResourceInfo rri : rd.scan(getDownloadFilter())) - download(listener, rri, adjusted.getChild(rri.getName())); + download(listener, rri, adjusted.getChild(rri.getName()), 0); // not supporting individual byte offsets for these files } finally { rd.close(); } @@ -128,13 +152,15 @@ private LocalDestFile downloadDir(final TransferListener listener, private LocalDestFile downloadFile(final StreamCopier.Listener listener, final RemoteResourceInfo remote, - final LocalDestFile local) + final LocalDestFile local, + final long byteOffset) throws IOException { final LocalDestFile adjusted = local.getTargetFile(remote.getName()); final RemoteFile rf = engine.open(remote.getPath()); try { - final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16); - final OutputStream os = adjusted.getOutputStream(); + log.debug("Attempting to download {} with offset={}", remote.getPath(), byteOffset); + final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16, byteOffset); + final OutputStream os = adjusted.getOutputStream(byteOffset != 0); try { new StreamCopier(rfis, os, engine.getLoggerFactory()) .bufSize(engine.getSubsystem().getLocalMaxPacketSize()) @@ -173,17 +199,17 @@ private Uploader(final LocalSourceFile source, final String remote) { this.remote = remote; } - private void upload(final TransferListener listener) throws IOException { + private void upload(final TransferListener listener, long byteOffset) throws IOException { if (source.isDirectory()) { makeDirIfNotExists(remote); // Ensure that the directory exists uploadDir(listener.directory(source.getName()), source, remote); setAttributes(source, remote); } else if (source.isFile() && isDirectory(remote)) { String adjustedRemote = engine.getPathHelper().adjustForParent(this.remote, source.getName()); - uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote); + uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote, byteOffset); setAttributes(source, adjustedRemote); } else if (source.isFile()) { - uploadFile(listener.file(source.getName(), source.getLength()), source, remote); + uploadFile(listener.file(source.getName(), source.getLength()), source, remote, byteOffset); setAttributes(source, remote); } else { throw new IOException(source + " is not a file or directory"); @@ -192,13 +218,14 @@ private void upload(final TransferListener listener) throws IOException { private void upload(final TransferListener listener, final LocalSourceFile local, - final String remote) + final String remote, + final long byteOffset) throws IOException { final String adjustedPath; if (local.isDirectory()) { adjustedPath = uploadDir(listener.directory(local.getName()), local, remote); } else if (local.isFile()) { - adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote); + adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote, byteOffset); } else { throw new IOException(local + " is not a file or directory"); } @@ -217,22 +244,34 @@ private String uploadDir(final TransferListener listener, throws IOException { makeDirIfNotExists(remote); for (LocalSourceFile f : local.getChildren(getUploadFilter())) - upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName())); + upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName()), 0); // not supporting individual byte offsets for these files return remote; } private String uploadFile(final StreamCopier.Listener listener, final LocalSourceFile local, - final String remote) + final String remote, + final long byteOffset) throws IOException { - final String adjusted = prepareFile(local, remote); + final String adjusted = prepareFile(local, remote, byteOffset); RemoteFile rf = null; InputStream fis = null; RemoteFile.RemoteFileOutputStream rfos = null; + EnumSet modes; try { - rf = engine.open(adjusted, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC)); + if (byteOffset == 0) { + // Starting at the beginning, overwrite/create + modes = EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC); + } else { + // Starting at some offset, append + modes = EnumSet.of(OpenMode.WRITE, OpenMode.APPEND); + } + + log.debug("Attempting to upload {} with offset={}", local.getName(), byteOffset); + rf = engine.open(adjusted, modes); fis = local.getInputStream(); - rfos = rf.new RemoteFileOutputStream(0, 16); + fis.skip(byteOffset); + rfos = rf.new RemoteFileOutputStream(byteOffset, 16); new StreamCopier(fis, rfos, engine.getLoggerFactory()) .bufSize(engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead()) .keepFlushing(false) @@ -294,7 +333,7 @@ private boolean isDirectory(final String remote) throws IOException { } } - private String prepareFile(final LocalSourceFile local, final String remote) + private String prepareFile(final LocalSourceFile local, final String remote, final long byteOffset) throws IOException { final FileAttributes attrs; try { @@ -309,7 +348,7 @@ private String prepareFile(final LocalSourceFile local, final String remote) if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) { throw new IOException("Trying to upload file " + local.getName() + " to path " + remote + " but that is a directory"); } else { - log.debug("probeFile: {} is a {} file that will be replaced", remote, attrs.getMode().getType()); + log.debug("probeFile: {} is a {} file that will be {}", remote, attrs.getMode().getType(), byteOffset > 0 ? "resumed" : "replaced"); return remote; } } diff --git a/src/main/java/net/schmizz/sshj/xfer/FileSystemFile.java b/src/main/java/net/schmizz/sshj/xfer/FileSystemFile.java index b759f9cb0..56a6ebc38 100644 --- a/src/main/java/net/schmizz/sshj/xfer/FileSystemFile.java +++ b/src/main/java/net/schmizz/sshj/xfer/FileSystemFile.java @@ -71,7 +71,13 @@ public InputStream getInputStream() @Override public OutputStream getOutputStream() throws IOException { - return new FileOutputStream(file); + return getOutputStream(false); + } + + @Override + public OutputStream getOutputStream(boolean append) + throws IOException { + return new FileOutputStream(file, append); } @Override diff --git a/src/main/java/net/schmizz/sshj/xfer/FileTransfer.java b/src/main/java/net/schmizz/sshj/xfer/FileTransfer.java index f3b08169b..0f18f88a4 100644 --- a/src/main/java/net/schmizz/sshj/xfer/FileTransfer.java +++ b/src/main/java/net/schmizz/sshj/xfer/FileTransfer.java @@ -31,6 +31,19 @@ public interface FileTransfer { void upload(String localPath, String remotePath) throws IOException; + /** + * This is meant to delegate to {@link #upload(LocalSourceFile, String)} with the {@code localPath} wrapped as e.g. + * a {@link FileSystemFile}. Appends to existing if {@code byteOffset} > 0. + * + * @param localPath + * @param remotePath + * @param byteOffset + * + * @throws IOException + */ + void upload(String localPath, String remotePath, long byteOffset) + throws IOException; + /** * This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g. * a {@link FileSystemFile}. @@ -43,6 +56,19 @@ void upload(String localPath, String remotePath) void download(String remotePath, String localPath) throws IOException; + /** + * This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g. + * a {@link FileSystemFile}. Appends to existing if {@code byteOffset} > 0. + * + * @param localPath + * @param remotePath + * @param byteOffset + * + * @throws IOException + */ + void download(String remotePath, String localPath, long byteOffset) + throws IOException; + /** * Upload {@code localFile} to {@code remotePath}. * @@ -54,6 +80,18 @@ void download(String remotePath, String localPath) void upload(LocalSourceFile localFile, String remotePath) throws IOException; + /** + * Upload {@code localFile} to {@code remotePath}. Appends to existing if {@code byteOffset} > 0. + * + * @param localFile + * @param remotePath + * @param byteOffset + * + * @throws IOException + */ + void upload(LocalSourceFile localFile, String remotePath, long byteOffset) + throws IOException; + /** * Download {@code remotePath} to {@code localFile}. * @@ -65,6 +103,18 @@ void upload(LocalSourceFile localFile, String remotePath) void download(String remotePath, LocalDestFile localFile) throws IOException; + /** + * Download {@code remotePath} to {@code localFile}. Appends to existing if {@code byteOffset} > 0. + * + * @param localFile + * @param remotePath + * @param byteOffset + * + * @throws IOException + */ + void download(String remotePath, LocalDestFile localFile, long byteOffset) + throws IOException; + TransferListener getTransferListener(); void setTransferListener(TransferListener listener); diff --git a/src/main/java/net/schmizz/sshj/xfer/LocalDestFile.java b/src/main/java/net/schmizz/sshj/xfer/LocalDestFile.java index 7e4296a9d..6a240cc40 100644 --- a/src/main/java/net/schmizz/sshj/xfer/LocalDestFile.java +++ b/src/main/java/net/schmizz/sshj/xfer/LocalDestFile.java @@ -20,8 +20,13 @@ public interface LocalDestFile { + long getLength(); + OutputStream getOutputStream() throws IOException; + + OutputStream getOutputStream(boolean append) + throws IOException; /** @return A child file/directory of this directory with given {@code name}. */ LocalDestFile getChild(String name); diff --git a/src/main/java/net/schmizz/sshj/xfer/scp/SCPFileTransfer.java b/src/main/java/net/schmizz/sshj/xfer/scp/SCPFileTransfer.java index e4afe6a17..5fe06da07 100644 --- a/src/main/java/net/schmizz/sshj/xfer/scp/SCPFileTransfer.java +++ b/src/main/java/net/schmizz/sshj/xfer/scp/SCPFileTransfer.java @@ -52,24 +52,49 @@ private SCPEngine newSCPEngine() { @Override public void upload(String localPath, String remotePath) throws IOException { - newSCPUploadClient().copy(new FileSystemFile(localPath), remotePath); + upload(localPath, remotePath, 0); + } + + @Override + public void upload(String localFile, String remotePath, long byteOffset) + throws IOException { + upload(new FileSystemFile(localFile), remotePath, byteOffset); } @Override public void download(String remotePath, String localPath) throws IOException { - download(remotePath, new FileSystemFile(localPath)); + download(remotePath, localPath, 0); + } + + @Override + public void download(String remotePath, String localPath, long byteOffset) throws IOException { + download(remotePath, new FileSystemFile(localPath), byteOffset); } @Override public void download(String remotePath, LocalDestFile localFile) throws IOException { + download(remotePath, localFile, 0); + } + + @Override + public void download(String remotePath, LocalDestFile localFile, long byteOffset) + throws IOException { + checkByteOffsetSupport(byteOffset); newSCPDownloadClient().copy(remotePath, localFile); } @Override public void upload(LocalSourceFile localFile, String remotePath) throws IOException { + upload(localFile, remotePath, 0); + } + + @Override + public void upload(LocalSourceFile localFile, String remotePath, long byteOffset) + throws IOException { + checkByteOffsetSupport(byteOffset); newSCPUploadClient().copy(localFile, remotePath); } @@ -79,4 +104,11 @@ public SCPFileTransfer bandwidthLimit(int limit) { } return this; } + + private void checkByteOffsetSupport(long byteOffset) throws IOException { + // TODO - implement byte offsets on SCP, if possible. + if (byteOffset > 0) { + throw new SCPException("Byte offset on SCP file transfers is not supported."); + } + } } diff --git a/src/test/java/com/hierynomus/sshj/test/util/FileUtil.java b/src/test/java/com/hierynomus/sshj/test/util/FileUtil.java index 10662bc15..a0767e122 100644 --- a/src/test/java/com/hierynomus/sshj/test/util/FileUtil.java +++ b/src/test/java/com/hierynomus/sshj/test/util/FileUtil.java @@ -39,4 +39,8 @@ public static String readFromFile(File f) throws IOException { IOUtils.closeQuietly(fileInputStream); } } + + public static boolean compareFileContents(File f1, File f2) throws IOException { + return readFromFile(f1).equals(readFromFile(f2)); + } } diff --git a/src/test/java/net/schmizz/sshj/sftp/SFTPFileTransferTest.java b/src/test/java/net/schmizz/sshj/sftp/SFTPFileTransferTest.java new file mode 100644 index 000000000..0c467d248 --- /dev/null +++ b/src/test/java/net/schmizz/sshj/sftp/SFTPFileTransferTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.schmizz.sshj.sftp; + +import com.hierynomus.sshj.test.SshFixture; +import com.hierynomus.sshj.test.util.FileUtil; +import java.io.File; +import java.io.IOException; +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.common.StreamCopier; +import net.schmizz.sshj.xfer.TransferListener; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SFTPFileTransferTest { + + public static final String TARGET_FILE_NAME = "target.txt"; + + File targetDir; + File targetFile; + File sourceFile; + + File partialFile; + + SSHClient sshClient; + SFTPFileTransfer xfer; + ByteCounter listener; + + @Rule + public SshFixture fixture = new SshFixture(); + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Before + public void init() throws IOException { + targetDir = tempFolder.newFolder(); + targetFile = new File(targetDir, TARGET_FILE_NAME); + sourceFile = new File("src/test/resources/files/test_file_full.txt"); + + partialFile = new File("src/test/resources/files/test_file_partial.txt"); + + sshClient = fixture.setupConnectedDefaultClient(); + sshClient.authPassword("test", "test"); + xfer = sshClient.newSFTPClient().getFileTransfer(); + xfer.setTransferListener(listener = new ByteCounter()); + } + + @After + public void cleanup() { + if (targetFile.exists()) { + targetFile.delete(); + } + + if (targetDir.exists()) { + targetDir.delete(); + } + } + + private void performDownload(long byteOffset) throws IOException { + assertTrue(listener.getBytesTransferred() == 0); + + long expectedBytes = 0; + + // Using the resume param this way to call the different entry points into the FileTransfer interface + if (byteOffset > 0) { + expectedBytes = sourceFile.length() - targetFile.length(); // only the difference between what is there and what should be + xfer.download(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath(), byteOffset); + } else { + expectedBytes = sourceFile.length(); // the entire source file should be transferred + xfer.download(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath()); + } + + assertTrue(FileUtil.compareFileContents(sourceFile, targetFile)); + assertTrue(listener.getBytesTransferred() == expectedBytes); + } + + private void performUpload(long byteOffset) throws IOException { + assertTrue(listener.getBytesTransferred() == 0); + + long expectedBytes = 0; + + // Using the resume param this way to call the different entry points into the FileTransfer interface + if (byteOffset > 0) { + expectedBytes = sourceFile.length() - targetFile.length(); // only the difference between what is there and what should be + xfer.upload(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath(), byteOffset); + } else { + expectedBytes = sourceFile.length(); // the entire source file should be transferred + xfer.upload(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath()); + } + assertTrue(FileUtil.compareFileContents(sourceFile, targetFile)); + assertTrue(listener.getBytesTransferred() == expectedBytes); + } + + @Test + public void testDownload() throws IOException { + performDownload(0); + } + + @Test + public void testDownloadResumePartial() throws IOException { + FileUtil.writeToFile(targetFile, FileUtil.readFromFile(partialFile)); + assertFalse(FileUtil.compareFileContents(sourceFile, targetFile)); + performDownload(targetFile.length()); + } + + @Test + public void testDownloadResumeNothing() throws IOException { + assertFalse(targetFile.exists()); + performDownload(targetFile.length()); + } + + @Test + public void testDownloadResumePreviouslyCompleted() throws IOException { + FileUtil.writeToFile(targetFile, FileUtil.readFromFile(sourceFile)); + assertTrue(FileUtil.compareFileContents(sourceFile, targetFile)); + performDownload(targetFile.length()); + } + + @Test + public void testUpload() throws IOException { + performUpload(0); + } + + @Test + public void testUploadResumePartial() throws IOException { + FileUtil.writeToFile(targetFile, FileUtil.readFromFile(partialFile)); + assertFalse(FileUtil.compareFileContents(sourceFile, targetFile)); + performUpload(targetFile.length()); + } + + @Test + public void testUploadResumeNothing() throws IOException { + assertFalse(targetFile.exists()); + performUpload(targetFile.length()); + } + + @Test + public void testUploadResumePreviouslyCompleted() throws IOException { + FileUtil.writeToFile(targetFile, FileUtil.readFromFile(sourceFile)); + assertTrue(FileUtil.compareFileContents(sourceFile, targetFile)); + performUpload(targetFile.length()); + } + + public class ByteCounter implements TransferListener, StreamCopier.Listener { + long bytesTransferred; + + public long getBytesTransferred() { + return bytesTransferred; + } + + @Override + public TransferListener directory(String name) { + return this; + } + + @Override + public StreamCopier.Listener file(String name, long size) { + return this; + } + + @Override + public void reportProgress(long transferred) throws IOException { + bytesTransferred = transferred; + } + } +} diff --git a/src/test/resources/files/test_file_full.txt b/src/test/resources/files/test_file_full.txt new file mode 100644 index 000000000..35a1e54ed --- /dev/null +++ b/src/test/resources/files/test_file_full.txt @@ -0,0 +1,10 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? \ No newline at end of file diff --git a/src/test/resources/files/test_file_partial.txt b/src/test/resources/files/test_file_partial.txt new file mode 100644 index 000000000..9c41be24f --- /dev/null +++ b/src/test/resources/files/test_file_partial.txt @@ -0,0 +1,6 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/? +abcdefghijklmnopqrstuvwxyzABCDEF \ No newline at end of file