1.0 能用
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
package net.bluemangoo.indpjsk.upload
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.bluemangoo.indpjsk.vpn.VpnLog
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.RandomAccessFile
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* 把抓到的响应体上传到工具箱:
|
||||
*
|
||||
* POST https://toolbox-api-direct.haruki.seiunx.com/ios/script/{upload_code}/upload
|
||||
* Content-Type: application/octet-stream (原始二进制,每片最大 1MB)
|
||||
* X-Script-Version: 1.0.1
|
||||
* X-Upload-Code: {upload_code}
|
||||
* X-Upload-Id: 9 位随机字母数字(同一文件的所有分片共用)
|
||||
* X-Chunk-Index: "0", "1", ...
|
||||
* X-Total-Chunks: 分片总数(ceil(size / 1MB))
|
||||
* X-Original-Url: 抓到的原始游戏 URL(后端用它识别区服 / UID / 数据类型)
|
||||
*
|
||||
* 分片串行发送,每一片都必须返回 HTTP 200。
|
||||
*/
|
||||
object Uploader {
|
||||
|
||||
private const val BASE = "https://toolbox-api-direct.haruki.seiunx.com/ios/script"
|
||||
private const val CHUNK_SIZE = 1024 * 1024
|
||||
private const val SCRIPT_VERSION = "1.0.1"
|
||||
|
||||
suspend fun upload(
|
||||
uploadCode: String,
|
||||
originalUrl: String,
|
||||
file: File,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
require(file.exists()) { "文件不存在: ${file.absolutePath}" }
|
||||
|
||||
val uploadId = randomUploadId()
|
||||
RandomAccessFile(file, "r").use { raf ->
|
||||
val size = raf.length()
|
||||
val total = if (size == 0L) 1 else ((size + CHUNK_SIZE - 1) / CHUNK_SIZE).toInt()
|
||||
val buf = ByteArray(CHUNK_SIZE)
|
||||
VpnLog.info("${file.name} 上传开始")
|
||||
|
||||
for (index in 0 until total) {
|
||||
val remaining = size - raf.filePointer
|
||||
val len = if (remaining <= 0L) 0 else minOf(CHUNK_SIZE.toLong(), remaining).toInt()
|
||||
if (len > 0) raf.readFully(buf, 0, len)
|
||||
val code = postChunk(uploadCode, uploadId, index, total, originalUrl, buf, len)
|
||||
if (code != 200) throw IOException("${file.name} 上传失败: HTTP $code")
|
||||
}
|
||||
VpnLog.info("${file.name} 上传完成")
|
||||
}
|
||||
}
|
||||
|
||||
private fun postChunk(
|
||||
uploadCode: String,
|
||||
uploadId: String,
|
||||
index: Int,
|
||||
total: Int,
|
||||
originalUrl: String,
|
||||
data: ByteArray,
|
||||
length: Int,
|
||||
): Int {
|
||||
val conn = URL("$BASE/$uploadCode/upload").openConnection() as HttpURLConnection
|
||||
try {
|
||||
conn.requestMethod = "POST"
|
||||
conn.doOutput = true
|
||||
conn.instanceFollowRedirects = false
|
||||
conn.connectTimeout = 15_000
|
||||
conn.readTimeout = 60_000
|
||||
conn.setRequestProperty("Content-Type", "application/octet-stream")
|
||||
conn.setRequestProperty("X-Script-Version", SCRIPT_VERSION)
|
||||
conn.setRequestProperty("X-Upload-Code", uploadCode)
|
||||
conn.setRequestProperty("X-Upload-Id", uploadId)
|
||||
conn.setRequestProperty("X-Chunk-Index", index.toString())
|
||||
conn.setRequestProperty("X-Total-Chunks", total.toString())
|
||||
conn.setRequestProperty("X-Original-Url", originalUrl)
|
||||
conn.setFixedLengthStreamingMode(length)
|
||||
conn.outputStream.use { it.write(data, 0, length) }
|
||||
|
||||
val code = conn.responseCode
|
||||
if (code != 200) {
|
||||
val body = runCatching {
|
||||
(conn.errorStream ?: conn.inputStream)
|
||||
?.bufferedReader()
|
||||
?.use { it.readText() }
|
||||
?.take(300)
|
||||
}.getOrNull().orEmpty()
|
||||
throw IOException("分片 ${index + 1}/$total 上传失败: HTTP $code $body")
|
||||
}
|
||||
return code
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/** 9 位随机字母数字,同一文件所有分片共用一个 */
|
||||
private fun randomUploadId(): String {
|
||||
val chars = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
return buildString(9) {
|
||||
repeat(9) { append(chars[Random.nextInt(chars.length)]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user