diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/Filesystem.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/Filesystem.kt index ac83e85..e3d6ed8 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/Filesystem.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/Filesystem.kt @@ -4,6 +4,7 @@ import org.domaindrivenarchitecture.provs.framework.core.platforms.SHELL import org.domaindrivenarchitecture.provs.framework.core.* import org.domaindrivenarchitecture.provs.framework.core.getLocalFileContent import java.io.File +import java.util.* /** @@ -109,7 +110,7 @@ fun Prov.createFile( val chunkedTest = text.chunked(maxBlockSize) for (chunk in chunkedTest) { // todo: consider usage of function addTextToFile - cmd( + cmdNoLog( "printf '%s' " + chunk .escapeAndEncloseByDoubleQuoteForShell() + " | $withSudo tee -a $fullyQualifiedFilename > /dev/null" ) @@ -145,28 +146,71 @@ fun Prov.deleteFile(file: String, path: String? = null, sudo: Boolean = false): fun Prov.fileContainsText(file: String, content: String, sudo: Boolean = false): Boolean { - // todo consider grep e.g. for content without newlines - // return cmdNoEval(prefixWithSudo("grep -- '${content.escapeSingleQuote()}' $file", sudo)).success - val fileContent = fileContent(file, sudo = sudo) - return if (fileContent == null) { - false + if (!checkFile(file, sudo)) { + return false + } + + // use grep for a single line or for a single line enclosed by a newline + if (!content.contains("\n") || (content.length >= 3 && !content.drop(1).dropLast(1).contains("\n"))) { + return cmdNoEval(prefixWithSudo("grep -- '${content.escapeSingleQuote().trim('\n')}' $file", sudo)).success } else { - fileContent.contains(content) + val fileContent = fileContent(file, sudo = sudo) + return fileContent?.contains(content) ?: false } } fun Prov.fileContent(file: String, sudo: Boolean = false): String? { - return cmdNoEval(prefixWithSudo("cat $file", sudo)).out + val largeFileSize = 40000 + + val size = fileSize(file, sudo) + if (size == null || size > largeFileSize) { + return fileContentLargeFile(file, sudo) + } else { + return cmdNoEval(prefixWithSudo("cat $file", sudo)).out + } } +fun Prov.fileContentLargeFile(file: String, sudo: Boolean = false, chunkSize: Int = 10000): String? { + require(chunkSize <= 40000) { "Chunk size must be < 40000" } + val maxSizeLargeFileContent = 10000000 // 10 MB + val size = fileSize(file, sudo) + if (size != null && size > maxSizeLargeFileContent) { + throw IllegalArgumentException("Cannot retrieve file content of files larger than: $maxSizeLargeFileContent bytes") + } -fun Prov.addTextToFile( - text: String, - file: String, - doNotAddIfExisting: Boolean = true, - sudo: Boolean = false -): ProvResult = addTextToFile(text, File(file), doNotAddIfExisting, sudo) + var offset = 0 + + var resultString: String? = null + do { + // todo : file paths starting with ~/ are not yet supported + val chunkResult = + cmdNoEval(prefixWithSudo("dd if=\"$file\" iflag=skip_bytes,count_bytes,fullblock bs=\"$chunkSize\" skip=\"$offset\" count=\"$chunkSize\" status=none | base64", sudo)) + + // check first chunk + if (resultString == null) { + if (!chunkResult.success) { + return resultString + } else { + resultString = "" + } + } + + val b = chunkResult.out?.trim() ?: "" + offset += chunkSize + + if (b.isEmpty() || b == "0") { + break + } + + // Use MimeDecoder to ignore newlines (\n) + val decodedBytes: ByteArray = Base64.getMimeDecoder().decode( b ) + val dec = String(decodedBytes) + resultString += dec + } while (true) + + return resultString +} fun Prov.addTextToFile( @@ -303,8 +347,8 @@ fun Prov.userHome(): String { /** * Returns number of bytes of a file or null if size could not be determined */ -fun Prov.fileSize(filename: String): Int? { - val result = cmd("wc -c < $filename") +fun Prov.fileSize(filename: String, sudo: Boolean = false): Int? { + val result = cmdNoEval("wc -c < $filename", sudo = sudo) return result.out?.trim()?.toIntOrNull() } diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/FilesystemKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/FilesystemKtTest.kt index 71da9b8..e43e14f 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/FilesystemKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/FilesystemKtTest.kt @@ -2,6 +2,7 @@ package org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base import org.domaindrivenarchitecture.provs.test.defaultTestContainer import org.domaindrivenarchitecture.provs.test.tags.ContainerTest +import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest import org.domaindrivenarchitecture.provs.test.testLocal import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test @@ -267,7 +268,7 @@ internal class FilesystemKtTest { defaultTestContainer().copyFileFromLocal("copiedFileFromLocal", "$resourcesDirectory/resource-test") // then - val content = defaultTestContainer().fileContent( "copiedFileFromLocal") + val content = defaultTestContainer().fileContent("copiedFileFromLocal") assertEquals("resource text\n", content) } @@ -275,18 +276,88 @@ internal class FilesystemKtTest { @ContainerTest fun fileContainsText() { // given - defaultTestContainer().createFile("testfilecontainingtext", "abc\n- def\nefg") + val file = "file_with_text" + defaultTestContainer().createFile(file, "\n\nabc\n- def\nefg\nhij\nklm\no\npq", sudo = true) // when - val res = defaultTestContainer().fileContainsText("testfilecontainingtext", "abc") - val res2 = defaultTestContainer().fileContainsText("testfilecontainingtext", "de") - val res3 = defaultTestContainer().fileContainsText("testfilecontainingtext", "- def") - val res4 = defaultTestContainer().fileContainsText("testfilecontainingtext", "xyy") + val res = defaultTestContainer().fileContainsText(file, "abc") + val res2 = defaultTestContainer().fileContainsText(file, "de") + val res3 = defaultTestContainer().fileContainsText(file, "- def") + val res4 = defaultTestContainer().fileContainsText(file, "xyy") + val res5 = defaultTestContainer().fileContainsText(file, "c\n- def\nefg\nhi") + val res6 = defaultTestContainer().fileContainsText(file, "\n\n") + val res7 = defaultTestContainer().fileContainsText(file, "\n\n\n") + val res8 = defaultTestContainer().fileContainsText(file, "\no\n") + val res10 = defaultTestContainer().fileContainsText(file, "\n\nabc") // then assertTrue(res) assertTrue(res2) assertTrue(res3) assertFalse(res4) + assertTrue(res5) + assertTrue(res6) + assertFalse(res7) + assertTrue(res8) + assertTrue(res10) + } + + @Test + @ContainerTest + fun fileContainsText_with_sudo() { + // given + val file = "sudotestfilecontainingtext" + defaultTestContainer().createFile(file, "abc\n- def\nefg\nhij\nklm\nop", sudo = true) + + // when + val res = defaultTestContainer().fileContainsText(file, "abc", sudo = true) + val res2 = defaultTestContainer().fileContainsText(file, "de", sudo = true) + val res3 = defaultTestContainer().fileContainsText(file, "- def", sudo = true) + val res4 = defaultTestContainer().fileContainsText(file, "xyy", sudo = true) + // test if newlines are recognized + val res5 = defaultTestContainer().fileContainsText(file, "c\n- def\nefg\nhi", sudo = true) + + // then + assertTrue(res) + assertTrue(res2) + assertTrue(res3) + assertFalse(res4) + assertTrue(res5) + } + + @ExtensiveContainerTest + fun fileContentLargeFile_success() { + // given + val prov = defaultTestContainer() + val filename = "largetestfile" + val content = "012345äöüß".repeat(100000) + + // when + val res = prov.createFile(filename, content, overwriteIfExisting = true) + val size = prov.fileSize(filename) + val actualContent = prov.fileContentLargeFile(filename, chunkSize = 40000) + + // then + assertTrue(res.success) + assertEquals(content, actualContent) + assertEquals(1400000, size) + } + + @ExtensiveContainerTest + fun fileContentLargeFile_with_sudo_success() { + // given + val prov = defaultTestContainer() + val filename = "largetestfile" + val content = "012345äöüß".repeat(100000) + + // when + val res = prov.createFile(filename, content, overwriteIfExisting = true, sudo = true) + val size = prov.fileSize(filename, sudo = true) + val actualContent = prov.fileContentLargeFile(filename, chunkSize = 40000, sudo = true) + + // then + assertTrue(res.success) + assertEquals(content, actualContent) + assertEquals(1400000, size) } }