Complete Code Examples

JVM: File Backup Script

A complete backup script that connects to a NAS share, creates a daily backup directory, uploads all files from a local folder, lists the results, and cleans up backups older than 7 days.

import com.ctreesoft.smb.SmbClient
import com.ctreesoft.smb.SmbConfig
import com.ctreesoft.smb.TransferChunkSize
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.io.File

fun main() = runBlocking {
    val config = SmbConfig(
        transferChunkSize = TransferChunkSize.ServerMax
    )

    val client = SmbClient(config)
    val share = client.connectShare(
        host = "nas.local",
        shareName = "backups",
        username = "backupuser",
        password = "s3cureP@ss",
        domain = "WORKGROUP"
    )

    try {
        // Create today's backup directory
        val today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)
        val backupDir = "daily/$today"
        share.createDirectory(backupDir)

        // Upload all files from the local source directory
        val sourceDir = File("/opt/appdata")
        val localFiles = sourceDir.listFiles() ?: emptyArray()

        for (file in localFiles) {
            if (file.isFile) {
                val remotePath = "$backupDir/${file.name}"
                share.writeFile(remotePath) { sink ->
                    sink.write(file.readBytes())
                }
                println("Uploaded: ${file.name}")
            }
        }

        // List everything in today's backup to confirm
        val uploaded = share.listDirectory(backupDir).toList()
        println("Backup complete: ${uploaded.size} files in $backupDir")

        // Clean up backups older than 7 days
        val cutoff = LocalDate.now().minusDays(7)
        val allBackups = share.listDirectory("daily").toList()

        for (entry in allBackups) {
            val dirDate = runCatching {
                LocalDate.parse(entry.name, DateTimeFormatter.ISO_LOCAL_DATE)
            }.getOrNull()

            if (dirDate != null && dirDate.isBefore(cutoff)) {
                share.deleteDirectory("daily/${entry.name}", recursive = true)
                println("Deleted old backup: ${entry.name}")
            }
        }
    } finally {
        share.close()
        client.close()
    }
}

Android: File Browser ViewModel

A ViewModel for an Android file browser that connects to an SMB share, navigates directories, and downloads files. Uses StateFlow for reactive UI updates and handles cleanup properly.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ctreesoft.smb.SmbClient
import com.ctreesoft.smb.SmbShare
import com.ctreesoft.smb.SmbDirectoryEntry
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import java.io.File

data class BrowserState(
    val connected: Boolean = false,
    val currentPath: String = "",
    val entries: List<SmbDirectoryEntry> = emptyList(),
    val loading: Boolean = false,
    val error: String? = null
)

class SmbBrowserViewModel : ViewModel() {

    private val _state = MutableStateFlow(BrowserState())
    val state: StateFlow<BrowserState> = _state

    private var client: SmbClient? = null
    private var share: SmbShare? = null

    fun connect(host: String, shareName: String, user: String, pass: String) {
        viewModelScope.launch {
            _state.value = _state.value.copy(loading = true, error = null)
            try {
                val smbClient = SmbClient()
                val smbShare = smbClient.connectShare(
                    host = host,
                    shareName = shareName,
                    username = user,
                    password = pass
                )
                client = smbClient
                share = smbShare

                _state.value = _state.value.copy(
                    connected = true,
                    loading = false
                )
                browse("")
            } catch (e: Exception) {
                _state.value = _state.value.copy(
                    loading = false,
                    error = "Connection failed: ${e.message}"
                )
            }
        }
    }

    fun browse(path: String) {
        val smbShare = share ?: return

        viewModelScope.launch {
            _state.value = _state.value.copy(loading = true, error = null)
            try {
                val entries = smbShare.listDirectory(path).toList()
                _state.value = _state.value.copy(
                    currentPath = path,
                    entries = entries,
                    loading = false
                )
            } catch (e: Exception) {
                _state.value = _state.value.copy(
                    loading = false,
                    error = "Browse failed: ${e.message}"
                )
            }
        }
    }

    fun downloadFile(remotePath: String, localDir: File) {
        val smbShare = share ?: return

        viewModelScope.launch {
            _state.value = _state.value.copy(loading = true, error = null)
            try {
                val fileName = remotePath.substringAfterLast("/")
                val localFile = File(localDir, fileName)

                smbShare.readFile(remotePath) { source ->
                    localFile.writeBytes(source.readByteArray())
                }

                _state.value = _state.value.copy(loading = false)
            } catch (e: Exception) {
                _state.value = _state.value.copy(
                    loading = false,
                    error = "Download failed: ${e.message}"
                )
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        share?.close()
        client?.close()
    }
}

BlackBerry Dynamics Integration

BlackBerry Dynamics (formerly Good Dynamics) requires all network traffic to go through its secure container. Use the SmbSocketFactory parameter to provide GDSocket instances so smb-kotlin routes traffic through the BlackBerry infrastructure.

import com.ctreesoft.smb.SmbClient
import com.ctreesoft.smb.SmbConfig
import com.ctreesoft.smb.SmbSocketFactory
import com.good.gd.net.GDSocket

suspend fun connectViaBBD(
    host: String,
    shareName: String,
    username: String,
    password: String
): SmbShare {
    val config = SmbConfig(
        socketFactory = SmbSocketFactory { GDSocket() }
    )

    val client = SmbClient(config)
    return client.connectShare(
        host = host,
        shareName = shareName,
        username = username,
        password = password
    )
}

The SmbSocketFactory callback is invoked each time smb-kotlin needs a new TCP connection. By returning a GDSocket, all SMB traffic is automatically routed through the BlackBerry Dynamics secure channel.

Server-Side: Processing Uploaded Files

A server-side function that scans an inbox folder on a file share for new CSV files, reads their content, and moves them to a processed folder after handling.

import com.ctreesoft.smb.SmbClient
import com.ctreesoft.smb.SmbShare
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.toList

suspend fun processNewUploads(share: SmbShare) {
    // Find all CSV files in the inbox
    val csvFiles = share.listDirectory("inbox")
        .filter { it.name.endsWith(".csv", ignoreCase = true) }
        .toList()

    println("Found ${csvFiles.size} CSV files to process")

    for (entry in csvFiles) {
        val remotePath = "inbox/${entry.name}"

        // Read the file content
        val content = StringBuilder()
        share.readFile(remotePath) { source ->
            content.append(source.readUtf8())
        }

        // Parse and process the CSV data
        val lines = content.lines()
        val header = lines.firstOrNull() ?: continue
        val dataRows = lines.drop(1).filter { it.isNotBlank() }

        println("Processing ${entry.name}: $header")
        println("  ${dataRows.size} data rows")

        // Move to processed folder after handling
        val destPath = "processed/${entry.name}"
        share.rename(remotePath, destPath)
        println("  Moved to $destPath")
    }
}

Streaming Large File Upload

Upload a large file to an SMB share without loading the entire file into memory. smb-kotlin uses Okio for I/O, so you can stream data directly from a local file source to the remote sink.

import com.ctreesoft.smb.SmbClient
import com.ctreesoft.smb.SmbConfig
import com.ctreesoft.smb.TransferChunkSize
import okio.buffer
import okio.source
import java.io.File

suspend fun uploadLargeFile(
    share: SmbShare,
    localFile: File,
    remotePath: String
) {
    share.writeFile(remotePath) { sink ->
        localFile.source().buffer().use { fileSource ->
            val buffer = ByteArray(8 * 1024 * 1024) // 8 MB chunks
            while (true) {
                val bytesRead = fileSource.read(buffer)
                if (bytesRead == -1) break
                sink.write(buffer, 0, bytesRead)
            }
        }
    }
}

// Usage
suspend fun main() {
    val config = SmbConfig(
        transferChunkSize = TransferChunkSize.ServerMax
    )
    val client = SmbClient(config)
    val share = client.connectShare(
        host = "fileserver.local",
        shareName = "uploads",
        username = "svcaccount",
        password = "p@ssword"
    )

    try {
        val bigFile = File("/data/database-export.sql.gz")
        uploadLargeFile(share, bigFile, "imports/${bigFile.name}")
        println("Upload complete: ${bigFile.length() / 1_048_576} MB transferred")
    } finally {
        share.close()
        client.close()
    }
}