SMBJ 简单使用指南 实现在 Java/Android 程序中访问 SMB 服务器

文章目录

一、问题背景

当我们在 Java 程序 或 Android 程序中,需要使用 SMB 的客户端连接 SMB 服务器进行上传和下载文件的时候,我们可以使用优秀的开源库 hierynomus/smbjhttps://github.com/hierynomus/smbj,其是专门为 Java 实现的 SMB2/SMB3 客户端库。其具有 API 简单清晰,使用方便的特点,本文将简单介绍其使用方法,实现文件/文件夹的上传、删除 等文件操作。

二、依赖导入

首先,我们需要在自己的项目中导入 SMBJ 的相关依赖

groovy 复制代码
dependencies {
    implementation 'com.hierynomus:smbj:0.14.0'
}

同时,在 Android 编译环境中,需要解决 META-INF/versions/9/OSGI-INF/MANIFEST.MF 文件冲突的问题,否则会报如下错误:

shell 复制代码
> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction
   > 2 files found with path 'META-INF/versions/9/OSGI-INF/MANIFEST.MF' from inputs:
      - org.bouncycastle:bcprov-jdk18on:1.79/bcprov-jdk18on-1.79.jar
      - org.jspecify:jspecify:1.0.0/jspecify-1.0.0.jar
     Adding a packaging block may help, please refer to
     https://developer.android.com/reference/tools/gradle-api/com/android/build/api/dsl/Packaging
     for more information

app 模块下的 build.gradle 文件的 android 包络中添加以下代码:

groovy 复制代码
android {
	packagingOptions {
        pickFirst 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
    }
}

三、连接到 SMB

SMBJ 对于文件的操作都是基于 com.hierynomus.smbj.share.DiskShare,因此我们需要先创建出 DiskShare 对象,

  • 在创建 DiskShare 对象,需要 com.hierynomus.smbj.session.Session 对象通过 connectShare() 方法连接到 SMB 的路径,
  • Session 对象是通过 com.hierynomus.smbj.connection.Connection 对象通过 authenticate 方法进行认证得到连接,
  • 最后 Connection 对象是通过 com.hierynomus.smbj.SMBClient 对象通过 connect 方法 连接到 SMB 服务器。

因此,连接的时序图如下:

核心代码如下:

kt 复制代码
// SMB 客户端对象
var client: SMBClient? 
// 文件操作对象,对应SMB的服务器的根目录
var diskShare: DiskShare?
try {
    client = SMBClient().apply {
        // 建立连接
        val connection = connect(SMB_IP)
        // 进行认证
        val session = connection.authenticate(AuthenticationContext(SMB_USERNAME,SMB_PASSWORD.toCharArray(),null))
        // 打开服务器的的根目录
        diskShare = session.connectShare(SMB_FOLDER) as? DiskShare
    }
} catch (e: Exception) {
	// 如果有异常 需要关闭 SMB 客户端对象
    client?.close()
}

这里有三个常量需要定义:

  • SMB_IPSMB 服务器的地址
  • SMB_USERNAMESMB 服务器登录用户名
  • SMB_PASSWORDSMB 服务器登录密码
  • SMB_FOLDERSMB 服务器的根目录

如果是通过匿名连接,则可以使用 AuthenticationContext.anonymous()AuthenticationContext.guest() 进行认证。

通过以上方法可以得到 DiskShare 对象进行操作文件。

四、文件夹创建

如果我们需要在 SMB 服务器创建一个新文件,则可以使用 com.hierynomus.smbj.utils.SmbFiles.mkdirs() 方法进行创建,同时此方法会递归创建父文件夹,保证父文件夹存在,文件夹可以创建成功。同时,如果创建失败,会抛出异常,只需要捕获此异常即可知道是创建失败,因此可以使用以下方法创建文件夹

kt 复制代码
var result = false
try {
    SmbFiles().mkdirs(diskShare, path)
    result = true
} catch (e: Exception) {
    result = false
}

这里的 path 是相对于 SMB 服务器的根目录地址,API文档如下:

之后,我们可以使用 DiskShare.folderExists() 方法检查文件夹是否存在

五、删除文件/文件夹

SMBJ 提供了两个方便的 API 进行删除文件和删除文件夹:

  • DiskShare.rm() 删除文件
  • DiskShare.rmdir() 删除文件夹

我们可以使用 DiskShare.fileExists()DiskShare.folderExists() 来检测需要删除的是文件还是文件夹,调用不同的 API 进行删除:

kt 复制代码
if (diskshare.folderExists(path)) {
    diskshare.rmdir(path, true)
} else if (diskshare.fileExists(path)) {
    diskshare.rm(path)
}

这里的 path 是相对于 SMB 服务器的根目录地址,API文档如下:

六、上传文件

文件上传有多种方法,但所有的方法的第一步是需要打开远端的文件,在打开文件的时候,可以传递不同的参数以实现不同的需求:

kt 复制代码
val file = diskShare.openFile(remotePath, EnumSet.of(AccessMask.GENERIC_WRITE), null,  EnumSet.of(SMB2ShareAccess.FILE_SHARE_WRITE), SMB2CreateDisposition.FILE_CREATE, null)

此方法的前面如下:

java 复制代码
public File openFile(String path, Set<AccessMask> accessMask, Set<FileAttributes> attributes, Set<SMB2ShareAccess> shareAccesses, SMB2CreateDisposition createDisposition, Set<SMB2CreateOptions> createOptions) {}
  • path 是相对于 SMB 服务器的根目录地址
  • accessMask 是用于控制文件的访问权限,默认传递 EnumSet.of(AccessMask.GENERIC_WRITE)
  • attributes 是文件的属性值,可以传空
  • shareAccesses 是操作类型,此处是写入文件,因此传递 EnumSet.of(SMB2ShareAccess.FILE_SHARE_WRITE)
  • createDisposition 是文件创建的方法,其可以传递以下类型的值
    • FILE_SUPERSEDE:如果文件存在时,则先删除旧文件,再写入新文件。如果文件不存在,则创建新文件
    • FILE_OPEN:如果文件存在时,则打开文件。如果文件不存在,则返回操作失败
    • FILE_CREATE:如果文件不存在,则创建新文件。如果文件存在,则返回操作失败
    • FILE_OPEN_IF:如果文件存在时,则打开文件。如果文件不存在,则创建文件
    • FILE_OVERWRITE:如果文件存在时,则覆写文件。如果文件不存在,则返回操作失败
    • FILE_OVERWRITE_IF:如果文件存在时,则覆写文件。如果文件不存在,则创建文件

随后我们需要创建本地的文件对象 localFile,打开本地文件的输入流。我们可以用如下方法写入文件:

kt 复制代码
// 打开本地文件的输入流
localFile.inputStream().buffered(UPLOAD_BUFFER_SIZE).use { input ->
	diskShare.openFile(remotePath, EnumSet.of(AccessMask.GENERIC_WRITE), null,  EnumSet.of(SMB2ShareAccess.FILE_SHARE_WRITE), SMB2CreateDisposition.FILE_CREATE, null).use { file ->
		// 打开远端文件的输出流
		file.outputStream.buffered(UPLOAD_BUFFER_SIZE).use { output ->
			// 将输入流写入到输出流中
			input.copyTo(output)
		}
	}
}
kt 复制代码
// 使用 FileByteChunkProvider 
FileByteChunkProvider(localFile).use { provider ->
	diskShare.openFile(remotePath, EnumSet.of(AccessMask.GENERIC_WRITE), null,  EnumSet.of(SMB2ShareAccess.FILE_SHARE_WRITE), SMB2CreateDisposition.FILE_CREATE, null).use { file ->
		// 将 provider 写入 file
		file.write(it)
	}
}

第二种方式在遇到大文件时会失败,建议使用第一种方法

同时,SmbFiles 提供了 copy()write() 的方法,方便上传文件。

七、Proguard 混淆规则

复制代码
-keep class org.slf4j.** { *; }
-dontwarn org.slf4j.**

-dontwarn javax.el.**
-dontwarn org.ietf.jgss.**
相关推荐
壮哥_icon4 小时前
【Android 系统开发】从掉帧卡顿到丝滑:高频硬件中断(IRQ)的 CPU 亲和性性能优化实战
android·性能优化
千里马学框架4 小时前
Android Framework 新手学习踩坑建议指南
android·智能手机·性能优化·framework·aaos·车载开发·系统工程师
其实防守也摸鱼5 小时前
运维--学习阶段问题解答(1)(自测)
linux·运维·服务器·数据库·学习·自动化·命令模式
懒鸟一枚5 小时前
深入理解 Linux 内存、Swap 交换分区与分页机制的关系
java·linux·数据库
GJGCY5 小时前
财务智能体落地实践:RPA、大模型与规则引擎如何构建业财自动化闭环?
经验分享·ai·自动化·财务·智能体
玖玥拾6 小时前
C# 语言进阶(十五)C# 游戏服务端 MySQL 数据库
服务器·开发语言·网络·数据库·mysql·c#
土星云SaturnCloud6 小时前
边缘计算驱动绿氢生产过程智能寻优:电解槽级实时优化技术解析
服务器·人工智能·ai·边缘计算
Darkwanderor6 小时前
对Linux的进程控制的研究
linux·运维·c++
我命由我123456 小时前
执行 Gradle 指令报错,无法将“grep”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
考虑考虑6 小时前
Sentinel安装
java·后端·微服务