feat: 图片上传 API

This commit is contained in:
HuanCheng65 2023-07-21 10:32:27 +08:00
parent d63d7a5a13
commit 8f3eddc006
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
5 changed files with 215 additions and 0 deletions

View File

@ -0,0 +1,34 @@
package com.huanchengfly.tieba.post.api.models
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UploadPictureResultBean(
@SerialName("error_code")
@SerializedName("error_code")
val errorCode: String,
@SerialName("error_msg")
@SerializedName("error_msg")
val errorMsg: String,
val resourceId: String,
val chunkNo: String,
val picId: String,
val picInfo: PicInfo
)
@Serializable
data class PicInfo(
val originPic: PicInfoItem,
val bigPic: PicInfoItem,
val smallPic: PicInfoItem,
)
@Serializable
data class PicInfoItem(
val width: String,
val height: String,
val type: String,
val picUrl: String
)

View File

@ -12,6 +12,15 @@ import okio.ByteString.Companion.encodeUtf8
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
fun buildMultipartBody(
boundary: String = UUID.randomUUID().toString(),
builder: MyMultipartBody.Builder.() -> Unit
): MyMultipartBody {
return MyMultipartBody.Builder(boundary)
.apply(builder)
.build()
}
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
class MyMultipartBody internal constructor( class MyMultipartBody internal constructor(
private val boundaryByteString: ByteString, private val boundaryByteString: ByteString,

View File

@ -17,6 +17,7 @@ import com.huanchengfly.tieba.post.utils.MobileInfoUtil
import com.huanchengfly.tieba.post.utils.UIDUtil import com.huanchengfly.tieba.post.utils.UIDUtil
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import okhttp3.RequestBody
import retrofit2.Call import retrofit2.Call
import retrofit2.http.* import retrofit2.http.*
@ -457,4 +458,16 @@ interface OfficialTiebaApi {
@Field("tbs") tbs: String = AccountUtil.getLoginInfo()!!.tbs, @Field("tbs") tbs: String = AccountUtil.getLoginInfo()!!.tbs,
@Field("stoken") stoken: String = AccountUtil.getSToken()!! @Field("stoken") stoken: String = AccountUtil.getSToken()!!
): Flow<AgreeBean> ): Flow<AgreeBean>
@Headers(
"${Header.FORCE_LOGIN}: ${Header.FORCE_LOGIN_TRUE}",
"${Header.DROP_HEADERS}: ${Header.CHARSET},${Header.CLIENT_TYPE}",
"${Header.NO_COMMON_PARAMS}: ${Param.SWAN_GAME_VER},${Param.SDK_VER}",
)
@POST("/c/s/uploadPicture")
fun uploadPicture(
@Body body: RequestBody,
@retrofit2.http.Header(Header.COOKIE) cookie: String = "ka=open;BAIDUID=${ClientUtils.baiduId}".takeIf { ClientUtils.baiduId != null }
?: "ka=open",
): Flow<UploadPictureResultBean>
} }

View File

@ -0,0 +1,132 @@
package com.huanchengfly.tieba.post.components
import android.graphics.BitmapFactory
import android.util.Log
import com.huanchengfly.tieba.post.api.BOUNDARY
import com.huanchengfly.tieba.post.api.booleanToString
import com.huanchengfly.tieba.post.api.models.UploadPictureResultBean
import com.huanchengfly.tieba.post.api.retrofit.RetrofitTiebaApi
import com.huanchengfly.tieba.post.api.retrofit.body.MyMultipartBody
import com.huanchengfly.tieba.post.api.retrofit.body.buildMultipartBody
import com.huanchengfly.tieba.post.api.retrofit.exception.TiebaException
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorCode
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
import com.huanchengfly.tieba.post.utils.MD5Util
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.withContext
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.io.RandomAccessFile
class ImageUploader(
private val forumName: String,
private val chunkSize: Int = DEFAULT_CHUNK_SIZE
) {
companion object {
const val DEFAULT_CHUNK_SIZE = 512000
const val IMAGE_MAX_SIZE = 5242880
const val ORIGIN_IMAGE_MAX_SIZE = 10485760
const val PIC_WATER_TYPE_NO = 0
const val PIC_WATER_TYPE_USER_NAME = 1
const val PIC_WATER_TYPE_FORUM_NAME = 2
}
fun uploadImages(
filePaths: List<String>,
isOriginImage: Boolean = false,
): Flow<List<UploadPictureResultBean>> {
return filePaths.asFlow()
.map { filePath ->
uploadSinglePicture(filePath, isOriginImage)
}
.runningFold<UploadPictureResultBean, MutableList<UploadPictureResultBean>>(initial = mutableListOf()) { list, result ->
list.add(result)
list
}
.filter { it.size == filePaths.size }
}
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun uploadSinglePicture(
filePath: String,
isOriginImage: Boolean = false,
): UploadPictureResultBean {
val option = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(filePath, option)
val width = option.outWidth
val height = option.outHeight
check(width > 0 && height > 0) { "图片宽高不正确" }
val file = File(filePath)
val fileLength = file.length()
val maxSize = if (isOriginImage) ORIGIN_IMAGE_MAX_SIZE else IMAGE_MAX_SIZE
check(fileLength <= maxSize) { "图片大小超过限制" }
val fileMd5 = MD5Util.toMd5(file)
val isMultipleChunkSize = fileLength % chunkSize == 0L
val totalChunkNum = fileLength / chunkSize + if (isMultipleChunkSize) 0 else 1
Log.i("ImageUploader", "fileLength=$fileLength, totalChunkNum=$totalChunkNum")
val requestBodies = (0 until totalChunkNum).map { chunk ->
val isFinish = chunk == totalChunkNum - 1
val curChunkSize = if (isFinish) {
if (isMultipleChunkSize) {
chunkSize
} else {
fileLength % chunkSize
}
} else {
chunkSize
}.toInt()
val chunkBytes = ByteArray(curChunkSize)
withContext(Dispatchers.IO) {
RandomAccessFile(file, "r").use {
it.seek(chunk * chunkSize.toLong())
it.read(chunkBytes)
}
}
buildMultipartBody(BOUNDARY) {
setType(MyMultipartBody.FORM)
addFormDataPart("alt", "json")
addFormDataPart("chunkNo", "${chunk + 1}")
if (forumName.isNotEmpty()) addFormDataPart("forum_name", forumName)
addFormDataPart("groupId", "1")
addFormDataPart("height", "$height")
addFormDataPart("isFinish", isFinish.booleanToString())
addFormDataPart("is_bjh", "0")
addFormDataPart("pic_water_type", "2")
addFormDataPart("resourceId", "$fileMd5$chunkSize")
addFormDataPart("saveOrigin", isOriginImage.booleanToString())
addFormDataPart("size", "$fileLength")
if (forumName.isNotEmpty()) addFormDataPart("small_flow_fname", forumName)
addFormDataPart("width", "$width")
addFormDataPart("chunk", "file", chunkBytes.toRequestBody())
}
}
return requestBodies.asFlow()
.flatMapConcat { RetrofitTiebaApi.OFFICIAL_TIEBA_API.uploadPicture(it) }
.catch {
throw UploadPictureFailedException(it.getErrorCode(), it.getErrorMessage())
}
.onEach {
Log.i("ImageUploader", "uploadSinglePicture: $it")
}
.last()
}
}
class UploadPictureFailedException(
override val code: Int = -1,
override val message: String = "上传图片失败",
) : TiebaException(message)

View File

@ -1,5 +1,9 @@
package com.huanchengfly.tieba.post.utils; package com.huanchengfly.tieba.post.utils;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -51,4 +55,27 @@ public class MD5Util {
} }
return null; return null;
} }
@NonNull
public static String toMd5(@NonNull File file) {
if (!file.isFile()) {
return "";
}
MessageDigest digest;
FileInputStream in;
byte[] buffer = new byte[1024];
int len;
try {
digest = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
while ((len = in.read(buffer, 0, 1024)) != -1) {
digest.update(buffer, 0, len);
}
in.close();
} catch (Exception e) {
e.printStackTrace();
return "";
}
return toHexString(digest.digest());
}
} }