From c9ac38de01eea1cdab05e2bbe773e2067d022a32 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Tue, 5 Mar 2024 17:56:16 -0500 Subject: [PATCH] [+] Optimize upload photo --- config/application.properties | 6 +- src/main/java/ext/Ext.kt | 14 ++- .../icu/samnyan/aqua/net/utils/PathProps.kt | 21 ++++ .../handler/impl/GetUserPortraitHandler.java | 2 +- .../handler/impl/UploadUserPhotoHandler.java | 70 ------------- .../handler/impl/UploadUserPhotoHandler.kt | 51 ++++++++++ .../impl/UploadUserPortraitHandler.java | 98 ------------------- .../handler/impl/UploadUserPortraitHandler.kt | 75 ++++++++++++++ 8 files changed, 165 insertions(+), 172 deletions(-) create mode 100644 src/main/java/icu/samnyan/aqua/net/utils/PathProps.kt delete mode 100644 src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPhotoHandler.java create mode 100644 src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPhotoHandler.kt delete mode 100644 src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPortraitHandler.java create mode 100644 src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPortraitHandler.kt diff --git a/config/application.properties b/config/application.properties index fbb49b04..904d7494 100644 --- a/config/application.properties +++ b/config/application.properties @@ -62,12 +62,14 @@ game.ongeki.rival.rivals-max-count=10 ## Maimai DX ## Allow users take photo as their avatar/portrait photo. game.maimai2.userPhoto.enable=true -## Specify folder path that user portrait photo and its (.json) data save to. -game.maimai2.userPhoto.picSavePath=data/userPhoto ## When uploading user portraits, limit the divMaxLength parameter. 1 divLength is about equal to the file size of 10kb. ## The default value is 32 (320kb), and the minimum value is 1 (10kb) game.maimai2.userPhoto.divMaxLength=32 +## User upload saving paths +paths.mai2-plays=data/upload/mai2/plays +paths.mai2-portrait=data/upload/mai2/portrait +paths.aqua-net-portrait=data/upload/net/portrait ## Logging spring.servlet.multipart.max-file-size=10MB diff --git a/src/main/java/ext/Ext.kt b/src/main/java/ext/Ext.kt index e575d409..d7348359 100644 --- a/src/main/java/ext/Ext.kt +++ b/src/main/java/ext/Ext.kt @@ -13,7 +13,9 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam +import java.nio.file.Path import java.time.LocalDate +import java.time.LocalDateTime import java.time.format.DateTimeFormatter typealias RP = RequestParam @@ -55,11 +57,21 @@ val HTTP = HttpClient(CIO) { fun millis() = System.currentTimeMillis() val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd") fun LocalDate.isoDate() = format(DATE_FORMAT) +fun LocalDateTime.isoDateTime() = format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) +// Encodings fun Long.toHex(len: Int = 16): Str = "0x${this.toString(len).padStart(len, '0').uppercase()}" fun Map.toUrl() = entries.joinToString("&") { (k, v) -> "$k=$v" } + +// Map operator fun Map.plus(map: Map) = (if (this is MutableMap) this else toMutableMap()).apply { putAll(map) } operator fun MutableMap.plusAssign(map: Map) { putAll(map) } -suspend fun async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() } \ No newline at end of file +suspend fun async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() } + +// Paths +fun path(part1: Str, vararg parts: Str) = Path.of(part1, *parts) +fun Str.path() = Path.of(this) +operator fun Path.div(part: Str) = resolve(part) + diff --git a/src/main/java/icu/samnyan/aqua/net/utils/PathProps.kt b/src/main/java/icu/samnyan/aqua/net/utils/PathProps.kt new file mode 100644 index 00000000..fa32727f --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/utils/PathProps.kt @@ -0,0 +1,21 @@ +package icu.samnyan.aqua.net.utils + +import ext.path +import jakarta.annotation.PostConstruct +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Configuration + +@Configuration +@ConfigurationProperties(prefix = "paths") +class PathProps { + var mai2Plays: String = "data/upload/mai2/plays" + var mai2Portrait: String = "data/upload/mai2/portrait" + var aquaNetPortrait: String = "data/upload/net/portrait" + + @PostConstruct + fun init() { + mai2Plays = mai2Plays.path().apply { toFile().mkdirs() }.toString() + mai2Portrait = mai2Portrait.path().apply { toFile().mkdirs() }.toString() + aquaNetPortrait = aquaNetPortrait.path().apply { toFile().mkdirs() }.toString() + } +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/GetUserPortraitHandler.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/GetUserPortraitHandler.java index 8f6df9bc..9f5cbd76 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/GetUserPortraitHandler.java +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/GetUserPortraitHandler.java @@ -34,7 +34,7 @@ public class GetUserPortraitHandler implements BaseHandler { public GetUserPortraitHandler(BasicMapper mapper, @Value("${game.maimai2.userPhoto.enable:true}") boolean enable, - @Value("${game.maimai2.userPhoto.picSavePath:data/userPhoto}") String picSavePath) { + @Value("${paths.mai2-portrait:data/userPhoto}") String picSavePath) { this.mapper = mapper; this.picSavePath = picSavePath; this.enable = enable; diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPhotoHandler.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPhotoHandler.java deleted file mode 100644 index b961d7b3..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPhotoHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.handler.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import icu.samnyan.aqua.sega.maimai2.handler.BaseHandler; -import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPhoto; -import icu.samnyan.aqua.sega.maimai2.model.request.data.UserPhoto; -import icu.samnyan.aqua.sega.util.jackson.BasicMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Base64; -import java.util.Map; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Component("Maimai2UploadUserPhotoHandler") -public class UploadUserPhotoHandler implements BaseHandler { - - private static final Logger logger = LoggerFactory.getLogger(UploadUserPhotoHandler.class); - - private final BasicMapper mapper; - - public UploadUserPhotoHandler(BasicMapper mapper) { - this.mapper = mapper; - } - - @Override - public String handle(Map request) throws JsonProcessingException { - /* - Maimai DX sends splited base64 data for one jpeg image. - So, make a temp file and keep append bytes until last part received. - If finished, rename it to other name so user can keep save multiple score cards in a single day. - */ - - UploadUserPhoto uploadUserPhoto = mapper.convert(request, UploadUserPhoto.class); - UserPhoto userPhoto = uploadUserPhoto.getUserPhoto(); - - long userId = userPhoto.getUserId(); - int trackNo = userPhoto.getTrackNo(); - - int divNumber = userPhoto.getDivNumber(); - int divLength = userPhoto.getDivLength(); - String divData = userPhoto.getDivData(); - - try { - String tmp_filename = userId + "-" + trackNo + ".tmp"; - byte[] imageData = Base64.getDecoder().decode(divData); - Files.write(Paths.get("data/" + tmp_filename), imageData, StandardOpenOption.CREATE, StandardOpenOption.APPEND); - - if (divNumber == (divLength - 1)) { - String filename = userId + "-" + LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) + ".jpg"; - Files.move(Paths.get("data/" + tmp_filename), Paths.get("data/" + filename)); - } - } catch (IOException e) { - logger.error("Result: User photo save failed", e); - } - - logger.info("Result: User photo saved"); - - return "{\"returnCode\":1,\"apiName\":\"com.sega.maimai2servlet.api.UploadUserPhotoApi\"}"; - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPhotoHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPhotoHandler.kt new file mode 100644 index 00000000..71e3efda --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPhotoHandler.kt @@ -0,0 +1,51 @@ +package icu.samnyan.aqua.sega.maimai2.handler.impl + +import ext.div +import ext.isoDateTime +import ext.path +import icu.samnyan.aqua.sega.maimai2.handler.BaseHandler +import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPhoto +import icu.samnyan.aqua.sega.util.jackson.BasicMapper +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import java.io.IOException +import java.nio.file.Files +import java.nio.file.StandardOpenOption.APPEND +import java.nio.file.StandardOpenOption.CREATE +import java.time.LocalDateTime +import java.util.* + +@Component("Maimai2UploadUserPhotoHandler") +class UploadUserPhotoHandler(private val mapper: BasicMapper) : BaseHandler { + val tmpDir = "data/tmp".path().apply { toFile().mkdirs() } + val uploadDir = "data/upload/mai2/plays".path().apply { toFile().mkdirs() } + + override fun handle(request: Map): String { + // Maimai DX sends split base64 data for one jpeg image. + // So, make a temp file and keep append bytes until last part received. + // If finished, rename it to other name so user can keep save multiple scorecards in a single day. + + val uploadUserPhoto = mapper.convert(request, UploadUserPhoto::class.java) + val up = uploadUserPhoto.userPhoto + + try { + val tmpFile = tmpDir / "${up.userId}-${up.trackNo}.tmp" + + Files.write(tmpFile, Base64.getDecoder().decode(up.divData), CREATE, APPEND) + + if (up.divNumber == (up.divLength - 1)) + Files.move(tmpFile, uploadDir / "${up.userId}-${LocalDateTime.now().isoDateTime()}.jpg") + } catch (e: IOException) { + logger.error("Result: User photo save failed", e) + } + + logger.info("Result: User photo saved") + + return """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.UploadUserPhotoApi"}""" + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(UploadUserPhotoHandler::class.java) + } +} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPortraitHandler.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPortraitHandler.java deleted file mode 100644 index fafd8f3c..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPortraitHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.handler.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import icu.samnyan.aqua.sega.maimai2.handler.BaseHandler; -import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPortrait; -import icu.samnyan.aqua.sega.util.jackson.BasicMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; -import java.util.Base64; -import java.util.Map; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Component("Maimai2UploadUserPortraitHandler") -public class UploadUserPortraitHandler implements BaseHandler { - - private static final Logger logger = LoggerFactory.getLogger(UploadUserPortraitHandler.class); - - private final BasicMapper mapper; - - private final String picSavePath; - private final boolean enable; - private final long divMaxLength; - - public UploadUserPortraitHandler(BasicMapper mapper, - @Value("${game.maimai2.userPhoto.enable:true}") boolean enable, - @Value("${game.maimai2.userPhoto.picSavePath:data/userPhoto}") String picSavePath, - @Value("${game.maimai2.userPhoto.divMaxLength:32}") long divMaxLength) { - this.mapper = mapper; - this.picSavePath = picSavePath; - this.enable = enable; - this.divMaxLength = divMaxLength; - - if (enable) { - try { - Files.createDirectories(Paths.get(picSavePath)); - } catch (Exception ignored) { - } - } - } - - @Override - public String handle(Map request) throws JsonProcessingException { - /* - Maimai DX sends splited base64 data for one jpeg image. - So, make a temp file and keep append bytes until last part received. - If finished, rename it to other name so user can keep save multiple score cards in a single day. - */ - - if (enable) { - var uploadUserPhoto = mapper.convert(request, UploadUserPortrait.class); - var userPhoto = uploadUserPhoto.getUserPortrait(); - - long userId = userPhoto.getUserId(); - int divNumber = userPhoto.getDivNumber(); - int divLength = userPhoto.getDivLength(); - String divData = userPhoto.getDivData(); - - if (divLength > divMaxLength) { - logger.warn(String.format("stop user %d uploading photo data because divLength(%d) > divMaxLength(%d)", userId, divLength, divMaxLength)); - return "{\"returnCode\":-1,\"apiName\":\"com.sega.maimai2servlet.api.UploadUserPortraitApi\"}"; - } - - try { - var tmp_filename = Paths.get(picSavePath, userId + "-up.tmp"); - if (divNumber == 0) - Files.deleteIfExists(tmp_filename); - - byte[] imageData = Base64.getDecoder().decode(divData); - Files.write(tmp_filename, imageData, StandardOpenOption.CREATE, StandardOpenOption.APPEND); - - logger.info(String.format("received user %d photo data %d/%d", userId, divNumber + 1, divLength)); - - if (divNumber == (divLength - 1)) { - var filename = Paths.get(picSavePath, userId + "-up.jpg"); - Files.move(tmp_filename, filename, StandardCopyOption.REPLACE_EXISTING); - - userPhoto.setDivData(""); - var userPortaitMetaJson = mapper.write(userPhoto); - var json_filename = Paths.get(picSavePath, userId + "-up.json"); - Files.write(json_filename, userPortaitMetaJson.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); - - logger.info(String.format("saved user %d photo data", userId)); - } - } catch (IOException e) { - logger.error("Result: User photo save failed", e); - } - } - return "{\"returnCode\":1,\"apiName\":\"com.sega.maimai2servlet.api.UploadUserPortraitApi\"}"; - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPortraitHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPortraitHandler.kt new file mode 100644 index 00000000..7c4f5a92 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/impl/UploadUserPortraitHandler.kt @@ -0,0 +1,75 @@ +package icu.samnyan.aqua.sega.maimai2.handler.impl + +import ext.div +import ext.path +import icu.samnyan.aqua.net.utils.PathProps +import icu.samnyan.aqua.sega.maimai2.handler.BaseHandler +import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPortrait +import icu.samnyan.aqua.sega.util.jackson.BasicMapper +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.io.IOException +import java.nio.file.Files +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.nio.file.StandardOpenOption.APPEND +import java.nio.file.StandardOpenOption.CREATE +import java.util.* +import kotlin.io.path.writeText + +@Component("Maimai2UploadUserPortraitHandler") +class UploadUserPortraitHandler( + val mapper: BasicMapper, + paths: PathProps, + @param:Value("\${game.maimai2.userPhoto.enable:true}") val enable: Boolean, + @param:Value("\${game.maimai2.userPhoto.divMaxLength:32}") val divMaxLength: Long +) : BaseHandler { + val path = paths.mai2Portrait.path() + val success = """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.UploadUserPortraitApi"}""" + val fail = """{"returnCode":-1,"apiName":"com.sega.maimai2servlet.api.UploadUserPortraitApi"}""" + + override fun handle(request: Map): String { + if (!enable) return success + + // Maimai DX sends split base64 data for one jpeg image. + // So, make a temp file and keep append bytes until last part received. + // If finished, rename it to other name so user can keep save multiple scorecards in a single day. + + val up = mapper.convert(request, UploadUserPortrait::class.java).userPortrait + + val id = up.userId + val num = up.divNumber + val len = up.divLength + + if (len > divMaxLength) { + logger.warn("stop user $id uploading photo data because divLength($len) > divMaxLength($divMaxLength)") + return fail + } + + try { + val tmp = path / "$id-up.tmp" + if (num == 0) Files.deleteIfExists(tmp) + Files.write(tmp, Base64.getDecoder().decode(up.divData), CREATE, APPEND) + + logger.info("> User photo $id data ${num + 1}/${len}") + + if (num == (len - 1)) { + Files.move(tmp, path / "$id-up.jpg", REPLACE_EXISTING) + + up.divData = "" + (path / "$id-up.json").writeText(mapper.write(up)) + + logger.info("> Saved user $id photo data") + } + } catch (e: IOException) { + logger.error("> User photo save failed", e) + } + + return success + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(UploadUserPortraitHandler::class.java) + } +}