Adding jCIFS To java.io.File For Easy Access To SMB Shares

Please see my project grootils on github where I collect such tools.

jCIFS from the Samba Project is a library to access SMB shares (used by Windows) directly from Java.

Instead of using this library directly and always keep its API in mind, I’d like to integrate it in java.io.File just by using a URL like smb://server/share/path/file when calling its constructor.

At first I wrote a Groovy Category, so I can use jCIFS like:

use (CifsCategory) {
    def f = new java.io.File("smb://server/share/path/file.pdf")
    f.smbCopyTo("/Users/rbe/tmp/a.pdf")
}
  • isSmb: is this file a SMB file?
  • toSmbFile: return a jcifs.smb.SmbFile object
  • smbCopyTo: copy a SMB file to another SMB or local file

These methods do the same as the counterpart from java.io.File but on a share using jCIFS:

  • smbList
  • smbListFiles
  • smbCreateNewFile
  • smbDelete
  • smbGetInputStream
  • smbNewInputStream
  • smbGetOutputStream
  • smbNewOutputStream
  • smbRenameTo

For the helper class GroovyHelper see below.

import com.bensmann.groovy.helper.GroovyHelper as GH

/**
 * Deal with SMB files using jCIFS.
 */
class CifsCategory {

    /**
     * Initialize with empty NTLM auth, due to: 
     * GroovyRuntimeException: Ambiguous method overloading for method jcifs.smb.SmbFile#.
     * Cannot resolve which method to invoke for [class java.lang.String, null]...
     */
    private static final NTLM_AUTH = new jcifs.smb.NtlmPasswordAuthentication("", "", "")

    /**
     * Is this a SmbFile?
     */
    def static isSmb(self) {
        switch (self.class) {
            case jcifs.smb.SmbFile:
                true
                break
            case java.io.File:
                self.path ==~ /smb:.*/
                break
            case java.lang.String:
                self ==~ /smb:.*/
                break
        }
    }

    /**
     * Create a SmbFile instance.
     */
    def static toSmbFile(self, ntlmAuth = NTLM_AUTH) {
        // Check argument
        if (self instanceof jcifs.smb.SmbFile) {
            return self
        } else /*if (self instanceof java.io.File)*/ {
            // Missing slashes; path will begin with smb:/ only and have to trailing slash
            // Windows: replace backslash
            def p = "smb://" + (self.path.replace('\', '/') - "smb:/") + "/"
            new jcifs.smb.SmbFile(p, ntlmAuth)
        }
    }

    /**
     * Create a new empty file.
     */
    def static smbCreateNewFile(self, ntlmAuth = NTLM_AUTH) {
        toSmbFile(self, ntlmAuth).createNewFile()
    }

    /**
     * List all files as String[].
     */
    def static smbList(self, ntlmAuth = NTLM_AUTH) {
        toSmbFile(self, ntlmAuth).list()
    }

    /**
     * List all files as File[].
     */
    def static smbListFiles(self, ntlmAuth = NTLM_AUTH) {
        // Convert all files to java.io.File to support our methods
        toSmbFile(self, ntlmAuth).listFiles().collect {
            // Create java.io.File and set NTLM authentication
            def f = new java.io.File(it.path)
            f.ntlmAuth = ntlmAuth
            f
        }
    }

    /**
     * Delete a file.
     */
    def static smbDelete(self, ntlmAuth = NTLM_AUTH) {
        toSmbFile(self, ntlmAuth).delete()
    }

    /**
     * Get an OutputStream.
     */
    def static smbGetOutputStream(self, ntlmAuth = NTLM_AUTH) {
        new jcifs.smb.SmbFileOutputStream(toSmbFile(self, ntlmAuth))
    }

    /**
     * Get an OutputStream.
     */
    def static smbNewOutputStream(self, ntlmAuth = NTLM_AUTH) {
        smbGetOutputStream(self, ntlmAuth)
    }

    /**
     * Get an InputStream.
     */
    def static smbGetInputStream(self, ntlmAuth = NTLM_AUTH) {
        new jcifs.smb.SmbFileInputStream(toSmbFile(self, ntlmAuth))
    }

    /**
     * Get an InputStream.
     */
    def static smbNewInputStream(self, ntlmAuth = NTLM_AUTH) {
        smbGetInputStream(self, ntlmAuth)
    }

    /**
     * Rename a file.
     */
    def static smbRenameTo(self, ntlmAuth = NTLM_AUTH, toFile) {
        toSmbFile(self, ntlmAuth).renameTo(toSmbFile(toFile, ntlmAuth))
    }

    /**
     * Copy a file, even to a local file:// URL.
     */
    def static smbCopyTo(self, ntlmAuth = NTLM_AUTH, toFile) {
        def to
        if (isSmb(toFile)) {
            to = toSmbFile(toFile, ntlmAuth)
        } else {
            to = toFile instanceof java.io.File ? toFile : new java.io.File(toFile)
        }
        to.createNewFile()
        // Copy file
        GH.copyStream smbGetInputStream(self, ntlmAuth), to.newOutputStream()
    }

}

The CifsInjector class provides additional methods:

  • copyTo: copy a local file to another SMB or local file
  • moveTo: move a file

CifsInjector tweaks java.io.File through invokeMethod by redirecting method calls to the category when the URL of a file begins with smb.

To use these methods directly through java.io.File we use Groovy’s Meta Object Protocol (see my post Method interception and synthesis with Groovy). I also added the copyTo() and moveTo() methods as it is convenient to do f.copyTo() with regular File-objects instead of copying streams each time.

import com.bensmann.groovy.helper.GroovyHelper as GH

/**
 * Inject CifsCategory methods into java.io.File. All files with URLs starting with smb:/ will
 * be handled using CifsCategory.
 */
class CifsFileInjector {

    /**
     * Dynamically call SMB-methods using invokeMethod.
     */
    def static inject = { ->
        // Copy a file.
        java.io.File.metaClass.copyTo = { toFile ->
            def to = toFile instanceof java.io.File ? toFile : new java.io.File(toFile)
            to.createNewFile()
            GH.copyStream delegate.newInputStream(), to.newOutputStream()
        }
        // Move a file.
        java.io.File.metaClass.moveTo = { toFile ->
            delegate.copyTo(toFile)
            delegate.delete()
        }
        // Delegate methods
        java.io.File.metaClass.ntlmAuth = CifsCategory.NTLM_AUTH
        java.io.File.metaClass.invokeMethod = { String name, args ->
            // Is this a SMB file; path starts with smb?
            if (CifsCategory.isSmb(delegate)) {
                use (CifsCategory) {
                    // Prefix method call with 'smb'
                    def n = name[0].toUpperCase() + name.substring(1)
                    delegate."smb${n}"(delegate.ntlmAuth, *args)
                }
            } else {
                def m = delegate.metaClass.getMetaMethod(name, *args)
                if (m) {
                    m.invoke(delegate, *args)
                } else {
                    throw new MissingMethodException(name, delegate.class, args)
                }
            }
        }
    }

}

Give it a try and copy a file from a SMB server:

CifsFileInjector.inject()
def f = new java.io.File("smb://server/share/path/document.pdf")
f.ntlmAuth = new jcifs.smb.NtlmPasswordAuthentication("domain", "user", "password")
f.copyTo("/Users/rbe/tmp/document.pdf") // delegated to smbCopyTo()

Or just list files in a directory:

CifsFileInjector.inject()
File f = new File("smb://server/share/path/")
f.ntlmAuth = new jcifs.smb.NtlmPasswordAuthentication("domain", "user", "pwd")
println f.list() // delegated to smbList()

Helper class GroovyHelper:

package com.bensmann.groovy.helper

class GroovyHelper {

    /**
     * Copy a stream using a certain buffer size.
     */
    def static copyStream = { from, to, bufKb = 1 ->
        byte[] buf = new byte[bufKb * 1024]
        def len = 0
        while ((len = from.read(buf)) > 0) {
            to.write(buf, 0, len)
        }
        to.close()
        from.close()
    }

}
This entry was posted in Groovy, Software Development and tagged . Bookmark the permalink.

Leave a Reply