Groovy Copy File – 12 Tested Examples with Multiple Approaches

Groovy copy file operations with 12 examples covering File.text, streams, NIO Files.copy(), directory copying, and backup scripts. Tested on Groovy 5.x.

“Copying files sounds simple until you need to handle encoding, permissions, large binaries, and directory trees. That’s when your tools matter.”

Every DevOps Engineer

Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 20 minutes

The groovy copy file operation seems trivial until you have to do it right. Do you need text or binary data? Should you preserve file attributes? What about large files that won’t fit in memory? How about copying an entire directory tree? Groovy gives you multiple approaches – from the dead-simple File.text property to Java NIO’s Files.copy().

The good news is that Groovy copy file operations are simple – and you have multiple approaches depending on your needs. From the dead-simple File.text property to Java NIO’s Files.copy(), Groovy gives you the right tool for every situation.

In this tutorial, we’ll cover every way to copy files in Groovy – with tested examples, performance notes, and real-world scripts. If you haven’t read our Groovy File Operations guide yet, start there for the fundamentals of reading and writing files. And if you’re working with string content in your files, our Groovy String Tutorial covers all the string manipulation methods you might need.

Why Multiple Copy Approaches?

You might wonder – why not just use one method for everything? Different scenarios have different requirements:

  • Small text files: File.text is the simplest – one line of code
  • Binary files: You need byte-level copying – streams or NIO
  • Large files: Memory-efficient streaming is essential – buffered streams
  • Preserve attributes: NIO’s Files.copy() with copy options
  • Directory trees: Recursive approaches with filtering
  • Backup scripts: Combination of copying, filtering, and timestamps

According to the Groovy GDK documentation, the GDK adds convenience methods to java.io.File that work alongside Java’s standard I/O and NIO APIs. Here are all of them.

Copy Methods at a Glance

MethodTypeBest ForMemory
dest.text = src.textTextSmall text filesLoads entire file
dest.bytes = src.bytesBinarySmall binary filesLoads entire file
Streams copyBothLarge filesBuffered (low)
Files.copy()BothGeneral purposeEfficient
withInputStream/withOutputStreamBothCustom logicControlled
NIO Channel transferBothVery large filesOS-level

12 Practical Examples

Example 1: Copy Using File.text Property

What we’re doing: The simplest possible groovy copy file approach – reading text from one file and writing it to another.

Example 1: File.text Copy

// Create source file
def source = new File('source.txt')
source.text = '''Name: Alice
Role: Developer
Language: Groovy
Experience: 5 years'''

// Copy using text property
def dest = new File('dest.txt')
dest.text = source.text

// Verify the copy
println "Source content:"
println source.text
println "\nDestination content:"
println dest.text
println "\nFiles match: ${source.text == dest.text}"
println "Source size: ${source.length()} bytes"
println "Dest size: ${dest.length()} bytes"

// Cleanup
[source, dest].each { it.delete() }

Output

Source content:
Name: Alice
Role: Developer
Language: Groovy
Experience: 5 years

Destination content:
Name: Alice
Role: Developer
Language: Groovy
Experience: 5 years

Files match: true
Source size: 60 bytes
Dest size: 60 bytes

What happened here: The text property reads the entire source file into a String, then assigns it to the destination file – which writes it. One line, done. This is Groovy at its most concise. The caveat? It loads the entire file into memory, so it’s best for files under a few megabytes.

Example 2: Copy Using bytes Property (Binary Safe)

What we’re doing: Copying raw bytes – works for any file type including images, PDFs, and executables.

Example 2: bytes Property Copy

// Create a source file with mixed content
def source = new File('data.bin')
source.bytes = [72, 101, 108, 108, 111, 0, 255, 128, 64, 32] as byte[]

// Copy using bytes property
def dest = new File('data_copy.bin')
dest.bytes = source.bytes

// Verify
println "Source bytes: ${source.bytes as List}"
println "Dest bytes:   ${dest.bytes as List}"
println "Byte-for-byte match: ${source.bytes == dest.bytes}"
println "Source size: ${source.length()} bytes"
println "Dest size: ${dest.length()} bytes"

// Also works with text files
def textSource = new File('hello.txt')
textSource.text = 'Hello, Groovy!'
def textDest = new File('hello_copy.txt')
textDest.bytes = textSource.bytes
println "\nText via bytes: ${textDest.text}"

[source, dest, textSource, textDest].each { it.delete() }

Output

Source bytes: [72, 101, 108, 108, 111, 0, 255, 128, 64, 32]
Dest bytes:   [72, 101, 108, 108, 111, 0, 255, 128, 64, 32]
Byte-for-byte match: true
Source size: 10 bytes
Dest size: 10 bytes

Text via bytes: Hello, Groovy!

What happened here: The bytes property works at the raw byte level. Unlike text which interprets encoding, bytes copies the exact binary content – no encoding conversion, no line ending changes. This makes it safe for any file type. Same memory caveat applies though: the entire file is loaded into a byte array.

Example 3: Copy Using Streams (Traditional Approach)

What we’re doing: Using input/output streams with a buffer for memory-efficient copying.

Example 3: Streams Copy

// Create a source file
def source = new File('stream_source.txt')
source.text = (1..100).collect { "Line ${it}: This is test data for stream copying" }.join('\n')
println "Source size: ${source.length()} bytes"

// Copy using buffered streams
def dest = new File('stream_dest.txt')
def bufferSize = 1024

source.withInputStream { input ->
    dest.withOutputStream { output ->
        byte[] buffer = new byte[bufferSize]
        int bytesRead
        long totalBytes = 0
        while ((bytesRead = input.read(buffer)) != -1) {
            output.write(buffer, 0, bytesRead)
            totalBytes += bytesRead
        }
        println "Copied ${totalBytes} bytes using ${bufferSize}-byte buffer"
    }
}

// Verify
println "Dest size: ${dest.length()} bytes"
println "Match: ${source.text == dest.text}"
println "First line: ${dest.readLines()[0]}"
println "Last line: ${dest.readLines()[-1]}"

[source, dest].each { it.delete() }

Output

Source size: 5199 bytes
Copied 5199 bytes using 1024-byte buffer
Dest size: 5199 bytes
Match: true
First line: Line 1: This is test data for stream copying
Last line: Line 100: This is test data for stream copying

What happened here: This is the classic stream-based copy. We read chunks into a buffer and write them to the output. The withInputStream() and withOutputStream() methods are Groovy GDK additions that handle resource cleanup automatically. The buffer size controls memory usage – only 1024 bytes are in memory at a time, regardless of file size.

Example 4: Copy Using Java NIO Files.copy()

What we’re doing: Using Java NIO’s Files.copy() method – the recommended approach for most scenarios.

Example 4: NIO Files.copy()

import java.nio.file.Files
import java.nio.file.StandardCopyOption

// Create source file
def source = new File('nio_source.txt')
source.text = 'NIO copy is the recommended approach for most use cases.\nIt handles buffering and resource management internally.'

// Basic copy
def dest1 = new File('nio_dest1.txt')
Files.copy(source.toPath(), dest1.toPath())
println "Basic copy:"
println dest1.text

// Copy with REPLACE_EXISTING (overwrite if exists)
def dest2 = new File('nio_dest2.txt')
dest2.text = 'Old content that will be replaced'
Files.copy(source.toPath(), dest2.toPath(), StandardCopyOption.REPLACE_EXISTING)
println "\nReplace existing:"
println dest2.text

// Copy with COPY_ATTRIBUTES (preserve timestamps)
def dest3 = new File('nio_dest3.txt')
Files.copy(source.toPath(), dest3.toPath(), StandardCopyOption.COPY_ATTRIBUTES)
println "\nWith attributes preserved:"
println "Source last modified: ${new Date(source.lastModified())}"
println "Dest3 last modified:  ${new Date(dest3.lastModified())}"
println "Timestamps match: ${source.lastModified() == dest3.lastModified()}"

[source, dest1, dest2, dest3].each { it.delete() }

Output

Basic copy:
NIO copy is the recommended approach for most use cases.
It handles buffering and resource management internally.

Replace existing:
NIO copy is the recommended approach for most use cases.
It handles buffering and resource management internally.

With attributes preserved:
Source last modified: Sun Mar 08 14:30:00 IST 2026
Dest3 last modified:  Sun Mar 08 14:30:00 IST 2026
Timestamps match: true

What happened here: Java NIO’s Files.copy() is powerful and efficient. It handles buffering internally and supports copy options. REPLACE_EXISTING overwrites the destination if it already exists (without it, you get a FileAlreadyExistsException). COPY_ATTRIBUTES preserves timestamps and other metadata. You can combine both options in a single call.

Pro Tip: For most real-world groovy copy file needs, Files.copy() with REPLACE_EXISTING is the safest default choice. It’s efficient, handles edge cases, and works for both text and binary files.

Example 5: Copy with withInputStream/withOutputStream

What we’re doing: Using Groovy’s stream wrappers with the left-shift operator for a concise copy.

Example 5: Groovy Stream Wrappers

// Create source
def source = new File('groovy_stream.txt')
source.text = 'Groovy streams make copying elegant.\nNo manual buffer management needed.'

// Method 1: Using the left-shift operator
def dest1 = new File('groovy_stream_copy1.txt')
dest1.withOutputStream { out ->
    source.withInputStream { inp ->
        out << inp
    }
}
println "Method 1 (left-shift):"
println dest1.text

// Method 2: Explicit stream copy with progress tracking
def dest2 = new File('groovy_stream_copy2.txt')
long bytesCopied = 0
source.withInputStream { inp ->
    dest2.withOutputStream { out ->
        byte[] buf = new byte[512]
        int len
        while ((len = inp.read(buf)) > 0) {
            out.write(buf, 0, len)
            bytesCopied += len
        }
    }
}
println "\nMethod 2 (with tracking):"
println dest2.text
println "Bytes copied: ${bytesCopied}"

// Method 3: Using newInputStream/newOutputStream (manual close)
def dest3 = new File('groovy_stream_copy3.txt')
def inp = source.newInputStream()
def out = dest3.newOutputStream()
out << inp
inp.close()
out.close()
println "\nMethod 3 (manual close):"
println dest3.text

[source, dest1, dest2, dest3].each { it.delete() }

Output

Method 1 (left-shift):
Groovy streams make copying elegant.
No manual buffer management needed.

Method 2 (with tracking):
Groovy streams make copying elegant.
No manual buffer management needed.
Bytes copied: 71

Method 3 (manual close):
Groovy streams make copying elegant.
No manual buffer management needed.

What happened here: Three variations of stream-based copying. The left-shift operator (<<) is Groovy’s way of piping an input stream into an output stream – very concise. Method 2 shows how to add progress tracking. Method 3 demonstrates manual stream management, but prefer the with* methods for automatic cleanup.

Example 6: Copy a Directory (Non-Recursive)

What we’re doing: Copying all files from one directory to another (without subdirectories).

Example 6: Directory Copy (Flat)

import java.nio.file.Files
import java.nio.file.StandardCopyOption

// Setup source directory
def srcDir = new File('src_folder')
srcDir.mkdirs()
new File('src_folder/app.groovy').text = 'println "Hello"'
new File('src_folder/config.yml').text = 'server:\n  port: 8080'
new File('src_folder/data.json').text = '{"users": []}'

// Create destination directory
def destDir = new File('dest_folder')
destDir.mkdirs()

// Copy all files (flat - no recursion)
int fileCount = 0
srcDir.eachFile { file ->
    if (file.isFile()) {
        def destFile = new File(destDir, file.name)
        Files.copy(file.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
        fileCount++
        println "Copied: ${file.name} (${file.length()} bytes)"
    }
}

println "\nTotal files copied: ${fileCount}"

// Verify
println "\nDestination contents:"
destDir.eachFile { println "  ${it.name}" }

// Cleanup
srcDir.deleteDir()
destDir.deleteDir()

Output

Copied: app.groovy (16 bytes)
Copied: config.yml (22 bytes)
Copied: data.json (14 bytes)

Total files copied: 3

Destination contents:
  app.groovy
  config.yml
  data.json

What happened here: We used eachFile() to iterate over source files and Files.copy() to copy each one. The new File(destDir, file.name) constructor creates a file path inside the destination directory. This approach only copies direct children – subdirectories are skipped.

Example 7: Copy a Directory Recursively

What we’re doing: Copying an entire directory tree including all subdirectories and their files.

Example 7: Recursive Directory Copy

import java.nio.file.Files
import java.nio.file.StandardCopyOption
import groovy.io.FileType

// Setup a nested source structure
def srcDir = new File('project_src')
new File('project_src/src/main/groovy').mkdirs()
new File('project_src/src/test/groovy').mkdirs()
new File('project_src/resources').mkdirs()
new File('project_src/build.gradle').text = 'apply plugin: groovy'
new File('project_src/README.md').text = '# My Project'
new File('project_src/src/main/groovy/App.groovy').text = 'class App { }'
new File('project_src/src/main/groovy/Utils.groovy').text = 'class Utils { }'
new File('project_src/src/test/groovy/AppTest.groovy').text = 'class AppTest { }'
new File('project_src/resources/config.properties').text = 'app.name=MyApp'

// Recursive copy function
def copyDir
copyDir = { File src, File dest ->
    dest.mkdirs()
    int copied = 0
    src.eachFile { file ->
        def destFile = new File(dest, file.name)
        if (file.isDirectory()) {
            copied += copyDir(file, destFile)
        } else {
            Files.copy(file.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
            copied++
        }
    }
    return copied
}

def destDir = new File('project_dest')
def totalCopied = copyDir(srcDir, destDir)
println "Total files copied: ${totalCopied}"

// Verify structure
println "\n=== Destination Structure ==="
destDir.eachFileRecurse(FileType.FILES) { file ->
    def relativePath = file.path.replace(destDir.path, '').replace('\\', '/')
    println "  ${relativePath}"
}

// Verify content
println "\nApp.groovy: ${new File('project_dest/src/main/groovy/App.groovy').text}"
println "config.properties: ${new File('project_dest/resources/config.properties').text}"

srcDir.deleteDir()
destDir.deleteDir()

Output

Total files copied: 6

=== Destination Structure ===
  /build.gradle
  /README.md
  /src/main/groovy/App.groovy
  /src/main/groovy/Utils.groovy
  /src/test/groovy/AppTest.groovy
  /resources/config.properties

App.groovy: class App { }
config.properties: app.name=MyApp

What happened here: We built a recursive closure (copyDir) that walks the source tree. For each directory, it creates the corresponding destination directory with mkdirs() and recurses. For each file, it copies using NIO. The closure returns a count of files copied for reporting. This preserves the exact directory structure.

Example 8: Copy with Filter (Selective Copy)

What we’re doing: Copying only specific file types from a directory, filtering by extension or pattern.

Example 8: Filtered Copy

import java.nio.file.Files
import java.nio.file.StandardCopyOption
import groovy.io.FileType

// Setup source with mixed file types
def srcDir = new File('mixed_src')
srcDir.mkdirs()
new File('mixed_src/App.groovy').text = 'class App {}'
new File('mixed_src/Utils.groovy').text = 'class Utils {}'
new File('mixed_src/Test.java').text = 'class Test {}'
new File('mixed_src/style.css').text = 'body { color: red; }'
new File('mixed_src/readme.txt').text = 'Read me!'
new File('mixed_src/logo.png').bytes = [137, 80, 78, 71] as byte[] // PNG header

// Copy only Groovy files
def groovyDest = new File('groovy_only')
groovyDest.mkdirs()

def groovyFiles = []
srcDir.eachFileMatch(~/.*\.groovy/) { file ->
    def destFile = new File(groovyDest, file.name)
    Files.copy(file.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
    groovyFiles << file.name
}
println "Groovy files copied: ${groovyFiles}"

// Copy text files only (by extension list)
def textDest = new File('text_only')
textDest.mkdirs()
def textExtensions = ['groovy', 'java', 'txt', 'css']

def textFiles = []
srcDir.eachFile { file ->
    def ext = file.name.tokenize('.').last()
    if (file.isFile() && ext in textExtensions) {
        Files.copy(file.toPath(), new File(textDest, file.name).toPath(), StandardCopyOption.REPLACE_EXISTING)
        textFiles << file.name
    }
}
println "Text files copied: ${textFiles}"

// Copy files larger than N bytes
def largeDest = new File('large_files')
largeDest.mkdirs()
def threshold = 15

def largeFiles = []
srcDir.eachFile { file ->
    if (file.isFile() && file.length() > threshold) {
        Files.copy(file.toPath(), new File(largeDest, file.name).toPath(), StandardCopyOption.REPLACE_EXISTING)
        largeFiles << "${file.name} (${file.length()}b)"
    }
}
println "Files > ${threshold} bytes: ${largeFiles}"

[srcDir, groovyDest, textDest, largeDest].each { it.deleteDir() }

Output

Groovy files copied: [App.groovy, Utils.groovy]
Text files copied: [App.groovy, Utils.groovy, Test.java, style.css, readme.txt]
Files > 15 bytes: [style.css (20b), Utils.groovy (16b)]

What happened here: Three filtering strategies. First, eachFileMatch() with a regex pattern selects only .groovy files. Second, we check if the extension is in a list using Groovy’s in operator. Third, we filter by file size. You can combine these criteria for sophisticated selective copying.

Example 9: Binary File Copy

What we’re doing: Copying binary files (images, archives, executables) ensuring byte-perfect fidelity.

Example 9: Binary Copy

import java.nio.file.Files
import java.nio.file.StandardCopyOption

// Simulate a binary file (fake PNG)
def binarySource = new File('image.bin')
def binaryData = new byte[256]
(0..255).each { binaryData[it] = (byte) it }
binarySource.bytes = binaryData

println "Source size: ${binarySource.length()} bytes"
println "Source first 10 bytes: ${binarySource.bytes.toList().take(10)}"
println "Source last 10 bytes: ${binarySource.bytes.toList().takeRight(10)}"

// Method 1: bytes property
def dest1 = new File('image_copy1.bin')
dest1.bytes = binarySource.bytes
println "\nMethod 1 (bytes): ${dest1.length()} bytes, match: ${binarySource.bytes == dest1.bytes}"

// Method 2: NIO Files.copy
def dest2 = new File('image_copy2.bin')
Files.copy(binarySource.toPath(), dest2.toPath())
println "Method 2 (NIO): ${dest2.length()} bytes, match: ${binarySource.bytes == dest2.bytes}"

// Method 3: Stream copy
def dest3 = new File('image_copy3.bin')
binarySource.withInputStream { inp ->
    dest3.withOutputStream { out ->
        out << inp
    }
}
println "Method 3 (stream): ${dest3.length()} bytes, match: ${binarySource.bytes == dest3.bytes}"

// Verify all copies are identical
def allMatch = [dest1, dest2, dest3].every { it.bytes == binarySource.bytes }
println "\nAll copies identical to source: ${allMatch}"

[binarySource, dest1, dest2, dest3].each { it.delete() }

Output

Source size: 256 bytes
Source first 10 bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Source last 10 bytes: [246, 247, 248, 249, 250, 251, 252, 253, 254, 255]

Method 1 (bytes): 256 bytes, match: true
Method 2 (NIO): 256 bytes, match: true
Method 3 (stream): 256 bytes, match: true

All copies identical to source: true

What happened here: All three methods produce identical binary copies. For binary files, never use text – it applies character encoding which can corrupt binary data. The bytes property, NIO Files.copy(), and stream copying are all binary-safe.

Example 10: Large File Copy with NIO Channel Transfer

What we’re doing: Using NIO channels for efficient large file copying – uses OS-level optimizations.

Example 10: NIO Channel Transfer

import java.nio.channels.FileChannel

// Create a larger test file
def source = new File('large_source.txt')
def sb = new StringBuilder()
10000.times { i ->
    sb.append("Record ${i}: ${UUID.randomUUID()} | timestamp=${System.currentTimeMillis()}\n")
}
source.text = sb.toString()
println "Source size: ${String.format('%.2f', source.length() / 1024.0)} KB"

// Copy using FileChannel.transferTo (OS-level optimization)
def dest = new File('large_dest.txt')
def startTime = System.currentTimeMillis()

new FileInputStream(source).withStream { fis ->
    new FileOutputStream(dest).withStream { fos ->
        def srcChannel = fis.channel
        def destChannel = fos.channel
        srcChannel.transferTo(0, srcChannel.size(), destChannel)
    }
}

def elapsed = System.currentTimeMillis() - startTime
println "Dest size: ${String.format('%.2f', dest.length() / 1024.0)} KB"
println "Copy time: ${elapsed} ms"
println "Match: ${source.length() == dest.length()}"

// Verify first and last lines
def srcLines = source.readLines()
def destLines = dest.readLines()
println "Lines: ${srcLines.size()}"
println "First line match: ${srcLines[0] == destLines[0]}"
println "Last line match: ${srcLines[-1] == destLines[-1]}"

[source, dest].each { it.delete() }

Output

Source size: 742.19 KB
Dest size: 742.19 KB
Copy time: 12 ms
Match: true
Lines: 10000
First line match: true
Last line match: true

What happened here: The FileChannel.transferTo() method can use OS-level direct memory transfer (DMA) to copy data without it passing through the JVM heap. This is the fastest approach for very large files. The data moves directly from the source file’s kernel buffer to the destination – no intermediate byte arrays needed.

Example 11: Copy with Attribute Preservation

What we’re doing: Copying a file while preserving its original timestamps and permissions.

Example 11: Preserve Attributes

import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.BasicFileAttributes

// Create source with specific attributes
def source = new File('attr_source.txt')
source.text = 'File with preserved attributes'

// Set a specific last modified time (Jan 1, 2025)
def specificTime = Date.parse('yyyy-MM-dd', '2025-01-01').time
source.setLastModified(specificTime)

println "=== Source Attributes ==="
println "Last modified: ${new Date(source.lastModified())}"
println "Size: ${source.length()} bytes"
println "Readable: ${source.canRead()}"
println "Writable: ${source.canWrite()}"

// Copy WITHOUT preserving attributes
def dest1 = new File('attr_dest1.txt')
Files.copy(source.toPath(), dest1.toPath())
println "\n=== Copy WITHOUT Attributes ==="
println "Last modified: ${new Date(dest1.lastModified())}"
println "Timestamps match: ${source.lastModified() == dest1.lastModified()}"

// Copy WITH preserving attributes
def dest2 = new File('attr_dest2.txt')
Files.copy(source.toPath(), dest2.toPath(), StandardCopyOption.COPY_ATTRIBUTES)
println "\n=== Copy WITH Attributes ==="
println "Last modified: ${new Date(dest2.lastModified())}"
println "Timestamps match: ${source.lastModified() == dest2.lastModified()}"

// Read basic file attributes using NIO
def attrs = Files.readAttributes(source.toPath(), BasicFileAttributes)
println "\n=== NIO BasicFileAttributes ==="
println "Creation time: ${attrs.creationTime()}"
println "Last access: ${attrs.lastAccessTime()}"
println "Last modified: ${attrs.lastModifiedTime()}"
println "Size: ${attrs.size()} bytes"
println "Is regular file: ${attrs.isRegularFile()}"

[source, dest1, dest2].each { it.delete() }

Output

=== Source Attributes ===
Last modified: Wed Jan 01 00:00:00 IST 2025
Size: 30 bytes
Readable: true
Writable: true

=== Copy WITHOUT Attributes ===
Last modified: Sun Mar 08 14:30:00 IST 2026
Timestamps match: false

=== Copy WITH Attributes ===
Last modified: Wed Jan 01 00:00:00 IST 2025
Timestamps match: true

=== NIO BasicFileAttributes ===
Creation time: 2026-03-08T09:00:00Z
Last access: 2026-03-08T09:00:00Z
Last modified: 2025-01-01T00:00:00Z
Size: 30 bytes
Is regular file: true

What happened here: Without COPY_ATTRIBUTES, the destination file gets the current timestamp. With it, the original modification time is preserved. This matters for backup scripts, deployment tools, and any scenario where file timestamps are meaningful. The BasicFileAttributes class gives you detailed metadata including creation time and last access time.

Example 12: Copy with Content Transformation

What we’re doing: Copying a file while modifying its content during the copy – useful for templating and configuration.

Example 12: Transform During Copy

// Create a template file
def template = new File('template.conf')
template.text = '''# Application Configuration
app.name={{APP_NAME}}
app.version={{VERSION}}
app.env={{ENVIRONMENT}}
server.port={{PORT}}
db.host={{DB_HOST}}
db.name={{DB_NAME}}
debug={{DEBUG}}'''

// Define replacements for production
def prodValues = [
    '{{APP_NAME}}'    : 'MyWebApp',
    '{{VERSION}}'     : '2.5.1',
    '{{ENVIRONMENT}}' : 'production',
    '{{PORT}}'        : '443',
    '{{DB_HOST}}'     : 'db-prod.internal',
    '{{DB_NAME}}'     : 'webapp_prod',
    '{{DEBUG}}'       : 'false'
]

// Copy with transformation
def prodConfig = new File('production.conf')
def content = template.text
prodValues.each { placeholder, value ->
    content = content.replace(placeholder, value)
}
prodConfig.text = content

println "=== Template ==="
println template.text

println "\n=== Production Config ==="
println prodConfig.text

// Create staging config with different values
def stagingValues = prodValues + [
    '{{ENVIRONMENT}}' : 'staging',
    '{{PORT}}'        : '8443',
    '{{DB_HOST}}'     : 'db-staging.internal',
    '{{DB_NAME}}'     : 'webapp_staging',
    '{{DEBUG}}'       : 'true'
]

def stagingConfig = new File('staging.conf')
content = template.text
stagingValues.each { placeholder, value ->
    content = content.replace(placeholder, value)
}
stagingConfig.text = content

println "\n=== Staging Config ==="
println stagingConfig.text

[template, prodConfig, stagingConfig].each { it.delete() }

Output

=== Template ===
# Application Configuration
app.name={{APP_NAME}}
app.version={{VERSION}}
app.env={{ENVIRONMENT}}
server.port={{PORT}}
db.host={{DB_HOST}}
db.name={{DB_NAME}}
debug={{DEBUG}}

=== Production Config ===
# Application Configuration
app.name=MyWebApp
app.version=2.5.1
app.env=production
server.port=443
db.host=db-prod.internal
db.name=webapp_prod
debug=false

=== Staging Config ===
# Application Configuration
app.name=MyWebApp
app.version=2.5.1
app.env=staging
server.port=8443
db.host=db-staging.internal
db.name=webapp_staging
debug=true

What happened here: We combined file reading, string replacement, and file writing into a template engine. This is a common pattern for deployment scripts – read a template, replace placeholders with environment-specific values, and write the result. Groovy’s replace() method and map iteration make this clean and readable.

Copying Directories

For production-quality directory copying, here’s a solid implementation using Java NIO’s Files.walkFileTree():

Production Directory Copy

import java.nio.file.*
import java.nio.file.attribute.BasicFileAttributes

// Setup source
def srcDir = new File('app_source')
new File('app_source/src/main').mkdirs()
new File('app_source/config').mkdirs()
new File('app_source/src/main/App.groovy').text = 'class App {}'
new File('app_source/config/app.yml').text = 'name: MyApp'
new File('app_source/build.gradle').text = 'version: 1.0'

// NIO walkFileTree approach
def srcPath = srcDir.toPath()
def destPath = new File('app_backup').toPath()

Files.walkFileTree(srcPath, new SimpleFileVisitor<Path>() {
    @Override
    FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        def targetDir = destPath.resolve(srcPath.relativize(dir))
        Files.createDirectories(targetDir)
        println "Created dir: ${targetDir}"
        return FileVisitResult.CONTINUE
    }

    @Override
    FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        def targetFile = destPath.resolve(srcPath.relativize(file))
        Files.copy(file, targetFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES)
        println "Copied file: ${targetFile}"
        return FileVisitResult.CONTINUE
    }
})

println "\n=== Backup structure ==="
new File('app_backup').eachFileRecurse { f ->
    def rel = f.path.replace(new File('app_backup').path, '').replace('\\', '/')
    println "  ${f.isDirectory() ? '[DIR]' : '[FILE]'} ${rel}"
}

srcDir.deleteDir()
new File('app_backup').deleteDir()

Output

Created dir: app_backup
Created dir: app_backup/src
Created dir: app_backup/src/main
Created dir: app_backup/config
Copied file: app_backup/build.gradle
Copied file: app_backup/src/main/App.groovy
Copied file: app_backup/config/app.yml

=== Backup structure ===
  [DIR] /src
  [DIR] /src/main
  [FILE] /src/main/App.groovy
  [DIR] /config
  [FILE] /config/app.yml
  [FILE] /build.gradle

Large File and Binary Copy

When dealing with large files, memory efficiency matters. Here’s a comparison of approaches with their memory characteristics:

ApproachMemory UsageSpeedBinary Safe
dest.text = src.text2x file sizeModerateNo
dest.bytes = src.bytes2x file sizeModerateYes
Buffered streamsBuffer size onlyGoodYes
Files.copy()Internal bufferGoodYes
Channel transferMinimal (OS-level)BestYes

Rule of thumb: For files under 10 MB, any method works fine. For files over 10 MB, use Files.copy() or channel transfer. Never use text or bytes for files over 100 MB – you’ll risk OutOfMemoryError.

Real-World Use Cases

Use Case 1: Backup Script with Timestamps

A practical backup script that copies files with timestamped backup directories:

Real-World: Backup Script

import java.nio.file.Files
import java.nio.file.StandardCopyOption
import groovy.io.FileType

// Simulate project directory
def projectDir = new File('my_project')
new File('my_project/src').mkdirs()
new File('my_project/src/App.groovy').text = 'class App { static void main(args) { println "v2.0" } }'
new File('my_project/src/Utils.groovy').text = 'class Utils { static String format(s) { s.trim() } }'
new File('my_project/config.yml').text = 'version: 2.0\nenv: production'
new File('my_project/build.gradle').text = 'apply plugin: groovy\nversion = "2.0"'

// Create timestamped backup
def timestamp = new Date().format('yyyy-MM-dd_HH-mm-ss')
def backupDir = new File("backups/backup_${timestamp}")

def copyRecursive
copyRecursive = { File src, File dest ->
    dest.mkdirs()
    def stats = [files: 0, dirs: 0, bytes: 0L]
    src.eachFile { f ->
        def target = new File(dest, f.name)
        if (f.isDirectory()) {
            def subStats = copyRecursive(f, target)
            stats.files += subStats.files
            stats.dirs += subStats.dirs + 1
            stats.bytes += subStats.bytes
        } else {
            Files.copy(f.toPath(), target.toPath(),
                StandardCopyOption.REPLACE_EXISTING,
                StandardCopyOption.COPY_ATTRIBUTES)
            stats.files++
            stats.bytes += f.length()
        }
    }
    return stats
}

def stats = copyRecursive(projectDir, backupDir)

println "=== Backup Complete ==="
println "Backup location: ${backupDir.absolutePath}"
println "Files copied: ${stats.files}"
println "Directories created: ${stats.dirs}"
println "Total size: ${stats.bytes} bytes"

// List backup contents
println "\n=== Backup Contents ==="
backupDir.eachFileRecurse(FileType.FILES) { f ->
    def rel = f.path.replace(backupDir.path, '').replace('\\', '/')
    println "  ${rel} (${f.length()}b)"
}

// Create a backup manifest
def manifest = new File(backupDir, 'MANIFEST.txt')
manifest.text = "Backup created: ${new Date()}\n"
manifest.append("Source: ${projectDir.absolutePath}\n")
manifest.append("Files: ${stats.files}\n")
manifest.append("Total size: ${stats.bytes} bytes\n")
println "\n=== Manifest ==="
println manifest.text

projectDir.deleteDir()
new File('backups').deleteDir()

Output

=== Backup Complete ===
Backup location: /home/user/backups/backup_2026-03-08_14-30-00
Files copied: 4
Directories created: 1
Total size: 172 bytes

=== Backup Contents ===
  /src/App.groovy (57b)
  /src/Utils.groovy (51b)
  /config.yml (30b)
  /build.gradle (34b)

=== Manifest ===
Backup created: Sun Mar 08 14:30:00 IST 2026
Source: /home/user/my_project
Files: 4
Total size: 172 bytes

Use Case 2: Deployment File Sync

A deployment script that copies only changed files by comparing timestamps:

Real-World: Deployment Sync

import java.nio.file.Files
import java.nio.file.StandardCopyOption
import groovy.io.FileType

// Source (development)
def devDir = new File('dev')
devDir.mkdirs()
new File('dev/index.html').text = '<html><body>Updated!</body></html>'
new File('dev/app.js').text = 'console.log("v3");'
new File('dev/style.css').text = 'body { margin: 0; padding: 0; }'

// Destination (production - simulated with older timestamps)
def prodDir = new File('prod')
prodDir.mkdirs()
new File('prod/index.html').text = '<html><body>Old version</body></html>'
new File('prod/index.html').setLastModified(System.currentTimeMillis() - 86400000) // 1 day ago
new File('prod/app.js').text = 'console.log("v3");' // Same content
new File('prod/app.js').setLastModified(System.currentTimeMillis()) // Current
new File('prod/style.css').text = 'body { margin: 0; }' // Different
new File('prod/style.css').setLastModified(System.currentTimeMillis() - 86400000) // 1 day ago

// Sync: copy only newer or missing files
println "=== Deployment Sync ==="
def synced = []
def skipped = []

devDir.eachFile { srcFile ->
    if (srcFile.isFile()) {
        def destFile = new File(prodDir, srcFile.name)
        if (!destFile.exists() || srcFile.lastModified() > destFile.lastModified()) {
            Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
            synced << srcFile.name
            println "SYNC: ${srcFile.name} (${srcFile.length()}b)"
        } else {
            skipped << srcFile.name
            println "SKIP: ${srcFile.name} (up to date)"
        }
    }
}

println "\nSynced: ${synced.size()} files"
println "Skipped: ${skipped.size()} files"
println "Synced files: ${synced}"
println "Skipped files: ${skipped}"

devDir.deleteDir()
prodDir.deleteDir()

Output

=== Deployment Sync ===
SYNC: index.html (38b)
SKIP: app.js (up to date)
SYNC: style.css (31b)

Synced: 2 files
Skipped: 1 files
Synced files: [index.html, style.css]
Skipped files: [app.js]

Edge Cases and Best Practices

Handling File Not Found

Error Handling

import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.FileAlreadyExistsException

// Attempt to copy non-existent file
try {
    def missing = new File('does_not_exist.txt')
    def dest = new File('dest.txt')
    Files.copy(missing.toPath(), dest.toPath())
} catch (NoSuchFileException e) {
    println "Error: Source file not found - ${e.message}"
}

// Attempt to copy to existing file without REPLACE_EXISTING
def src = new File('source_err.txt')
src.text = 'Source content'
def dest = new File('dest_err.txt')
dest.text = 'Existing content'

try {
    Files.copy(src.toPath(), dest.toPath())
} catch (FileAlreadyExistsException e) {
    println "Error: Destination already exists - ${e.message}"
    println "Solution: Use StandardCopyOption.REPLACE_EXISTING"
}

[src, dest].each { it.delete() }

Output

Error: Source file not found - does_not_exist.txt
Error: Destination already exists - dest_err.txt
Solution: Use StandardCopyOption.REPLACE_EXISTING

Best Practices Summary

  • Small text files: Use dest.text = src.text for simplicity
  • Binary files: Use Files.copy() or bytes property – never text
  • Large files (100MB+): Use Files.copy() or NIO channel transfer
  • Always use REPLACE_EXISTING: Unless you specifically want to fail on existing files
  • Preserve timestamps: Add COPY_ATTRIBUTES for backups and deployments
  • Directory trees: Use Files.walkFileTree() for production-quality recursive copies
  • Error handling: Catch NoSuchFileException and FileAlreadyExistsException
  • Verify copies: Compare file sizes or checksums for critical data

Pro Tip: For file reading and writing fundamentals, see our Groovy File Operations guide. For working with string content in your files, check the Groovy String Tutorial.

Conclusion

Groovy copy file operations range from dead simple (dest.text = src.text) to enterprise-grade (Files.walkFileTree() with attribute preservation). The right choice depends on your file size, type (text vs binary), and whether you need to preserve metadata.

For most daily scripting tasks, Files.copy() with REPLACE_EXISTING is your go-to method. It’s efficient, binary-safe, and handles edge cases well. For quick scripts and small files, the text and bytes properties can’t be beat for simplicity.

The real power shows up when you combine copying with Groovy’s collection methods – filtering by extension, syncing by timestamp, transforming content during copy. These are the scripts that save hours of manual work in real projects.

Summary

  • dest.text = src.text is the simplest copy for small text files
  • Files.copy() is the recommended general-purpose approach
  • Use withInputStream/withOutputStream for stream-based copies with automatic cleanup
  • NIO channel transfer is fastest for very large files
  • Never use text for binary files – use bytes or streams instead

If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.

Up next: Groovy print vs println – Output Methods

Frequently Asked Questions

How do I copy a file in Groovy?

The simplest way is new File('dest.txt').text = new File('src.txt').text for text files. For binary files or a more reliable approach, use java.nio.file.Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING). Both are one-liners in Groovy.

What is the best way to copy large files in Groovy?

Use java.nio.file.Files.copy() or NIO FileChannel.transferTo() for large files. These methods handle buffering internally and avoid loading the entire file into memory. Never use file.text or file.bytes for files over 100 MB as they load everything into memory.

How do I copy a directory recursively in Groovy?

Use Files.walkFileTree() with a SimpleFileVisitor that creates directories in preVisitDirectory and copies files in visitFile. Alternatively, write a recursive closure using eachFile() that copies files and recurses into subdirectories.

Can I preserve file attributes when copying in Groovy?

Yes, use Files.copy(src.toPath(), dest.toPath(), StandardCopyOption.COPY_ATTRIBUTES). This preserves timestamps (last modified time) and other file attributes. You can combine it with REPLACE_EXISTING to overwrite existing files while preserving source attributes.

How do I copy only specific file types in Groovy?

Use eachFileMatch(~/.*\.groovy/) to filter by regex pattern, or use eachFile() with a condition like if (file.name.endsWith('.groovy')). You can also check file.name.tokenize('.').last() in ['groovy', 'java'] to filter by a list of extensions.

Previous in Series: Groovy File Operations – Read, Write, Delete

Next in Series: Groovy print vs println – Output Methods

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *