You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用KMongo在Ktor项目中将图片存储到MongoDB集合中

Ktor + KMongo 实现图片存储到MongoDB的实现思路

实现原理说明

MongoDB存储二进制文件有两种主流方案:

  • 单文件体积小于16MB时,直接用BSON的Binary字段存储即可,实现最简单
  • 单文件体积大于等于16MB时,使用MongoDB原生的GridFS组件存储,KMongo已经封装了GridFS的全量操作API,无需额外引入依赖

前置依赖配置

在build.gradle.kts中添加核心依赖:

// 适配Ktor协程模型的KMongo依赖
implementation("org.litote.kmongo:kmongo-coroutine:4.10.0")
// Ktor多部分表单上传支持
implementation("io.ktor:ktor-server-core:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")

方案1:小体积图片直接存储(<16MB)

1. 定义数据存储实体

import org.litote.kmongo.Id
import org.litote.kmongo.newId

data class Image(
    val id: Id<Image> = newId(),
    val fileName: String,
    val contentType: String,
    val size: Long,
    val content: ByteArray // 存储图片二进制内容
) {
    // 重写方法解决ByteArray默认比较的问题
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        other as Image
        return id == other.id && content.contentEquals(other.content)
    }

    override fun hashCode(): Int {
        var result = id.hashCode()
        result = 31 * result + content.contentHashCode()
        return result
    }
}

2. 实现图片上传接口

import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.content.*
import org.litote.kmongo.coroutine.CoroutineDatabase

fun Route.imageUploadRoute(db: CoroutineDatabase) {
    val imageCol = db.getCollection<Image>("images")
    post("/upload/image") {
        val multipart = call.receiveMultipart()
        var saveResult: String? = null
        multipart.forEachPart { part ->
            if (part is PartData.FileItem) {
                val byteContent = part.streamProvider().readBytes()
                val image = Image(
                    fileName = part.originalFileName ?: "unnamed_image",
                    contentType = part.contentType?.toString() ?: "image/jpeg",
                    size = byteContent.size.toLong(),
                    content = byteContent
                )
                imageCol.insertOne(image)
                saveResult = image.id.toString()
            }
            part.dispose()
        }
        saveResult?.let { 
            call.respond(mapOf("code" to 200, "imageId" to it)) 
            return@post
        }
        call.respond(mapOf("code" to 400, "msg" to "未读取到有效图片"))
    }
}

3. 实现图片读取接口

get("/image/{id}") {
    val imageId = call.parameters["id"] ?: return@get call.respond(mapOf("code" to 400, "msg" => "缺少图片ID"))
    val image = db.getCollection<Image>("images").findById(imageId) ?: return@get call.respond(mapOf("code" to 404, "msg" => "图片不存在"))
    call.respondBytes(image.content, io.ktor.http.ContentType.parse(image.contentType))
}

方案2:大体积图片GridFS存储(>=16MB)

1. 实现大文件上传接口

post("/upload/large-image") {
    val multipart = call.receiveMultipart()
    val gridFs = db.getGridFS()
    var fileId: String? = null
    multipart.forEachPart { part ->
        if (part is PartData.FileItem) {
            val fileName = part.originalFileName ?: "unnamed_large_image"
            val contentType = part.contentType?.toString() ?: "image/jpeg"
            // 直接将流写入GridFS,无需一次性读取全部内容到内存
            val id = gridFs.uploadFromStream(
                fileName,
                part.streamProvider(),
                org.bson.Document("contentType", contentType)
            )
            fileId = id.toString()
        }
        part.dispose()
    }
    fileId?.let {
        call.respond(mapOf("code" to 200, "fileId" to it))
        return@post
    }
    call.respond(mapOf("code" to 400, "msg" to "未读取到有效文件"))
}

2. 实现大文件读取接口

get("/large-image/{id}") {
    val fileId = call.parameters["id"] ?: return@get call.respond(mapOf("code" to 400, "msg" => "缺少文件ID"))
    val gridFs = db.getGridFS()
    val file = gridFs.find(org.bson.Document("_id", org.bson.types.ObjectId(fileId))).first() ?: return@get call.respond(mapOf("code" to 404, "msg" => "文件不存在"))
    val outStream = java.io.ByteArrayOutputStream()
    gridFs.downloadToStream(file.id, outStream)
    val contentType = file.metadata?.getString("contentType") ?: "image/jpeg"
    call.respondBytes(outStream.toByteArray(), io.ktor.http.ContentType.parse(contentType))
}

注意事项

  • 生产环境需要添加文件格式、大小校验逻辑,避免恶意文件上传
  • 高频访问的图片建议加二级缓存,降低MongoDB的读取压力
  • 高并发场景下建议GridFS和业务数据库集群物理分离部署

内容的提问来源于stack exchange,提问作者Astro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 09:48:03