feat: 图片上传 API
This commit is contained in:
parent
d63d7a5a13
commit
8f3eddc006
|
|
@ -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
|
||||
)
|
||||
|
|
@ -12,6 +12,15 @@ import okio.ByteString.Companion.encodeUtf8
|
|||
import java.io.IOException
|
||||
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")
|
||||
class MyMultipartBody internal constructor(
|
||||
private val boundaryByteString: ByteString,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.huanchengfly.tieba.post.utils.MobileInfoUtil
|
|||
import com.huanchengfly.tieba.post.utils.UIDUtil
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.*
|
||||
|
||||
|
|
@ -457,4 +458,16 @@ interface OfficialTiebaApi {
|
|||
@Field("tbs") tbs: String = AccountUtil.getLoginInfo()!!.tbs,
|
||||
@Field("stoken") stoken: String = AccountUtil.getSToken()!!
|
||||
): 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>
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
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.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
|
@ -51,4 +55,27 @@ public class MD5Util {
|
|||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue