mirror of https://github.com/hykilpikonna/AquaDX
[+] Optimize upload photo
parent
b9c063c41e
commit
c9ac38de01
|
@ -62,12 +62,14 @@ game.ongeki.rival.rivals-max-count=10
|
||||||
## Maimai DX
|
## Maimai DX
|
||||||
## Allow users take photo as their avatar/portrait photo.
|
## Allow users take photo as their avatar/portrait photo.
|
||||||
game.maimai2.userPhoto.enable=true
|
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.
|
## 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)
|
## The default value is 32 (320kb), and the minimum value is 1 (10kb)
|
||||||
game.maimai2.userPhoto.divMaxLength=32
|
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
|
## Logging
|
||||||
spring.servlet.multipart.max-file-size=10MB
|
spring.servlet.multipart.max-file-size=10MB
|
||||||
|
|
|
@ -13,7 +13,9 @@ import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestHeader
|
import org.springframework.web.bind.annotation.RequestHeader
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import java.nio.file.Path
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
typealias RP = RequestParam
|
typealias RP = RequestParam
|
||||||
|
@ -55,11 +57,21 @@ val HTTP = HttpClient(CIO) {
|
||||||
fun millis() = System.currentTimeMillis()
|
fun millis() = System.currentTimeMillis()
|
||||||
val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||||
fun LocalDate.isoDate() = format(DATE_FORMAT)
|
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 Long.toHex(len: Int = 16): Str = "0x${this.toString(len).padStart(len, '0').uppercase()}"
|
||||||
fun Map<String, Any>.toUrl() = entries.joinToString("&") { (k, v) -> "$k=$v" }
|
fun Map<String, Any>.toUrl() = entries.joinToString("&") { (k, v) -> "$k=$v" }
|
||||||
|
|
||||||
|
// Map
|
||||||
operator fun <K, V> Map<K, V>.plus(map: Map<K, V>) =
|
operator fun <K, V> Map<K, V>.plus(map: Map<K, V>) =
|
||||||
(if (this is MutableMap) this else toMutableMap()).apply { putAll(map) }
|
(if (this is MutableMap) this else toMutableMap()).apply { putAll(map) }
|
||||||
operator fun <K, V> MutableMap<K, V>.plusAssign(map: Map<K, V>) { putAll(map) }
|
operator fun <K, V> MutableMap<K, V>.plusAssign(map: Map<K, V>) { putAll(map) }
|
||||||
|
|
||||||
suspend fun <T> async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() }
|
suspend fun <T> 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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ public class GetUserPortraitHandler implements BaseHandler {
|
||||||
|
|
||||||
public GetUserPortraitHandler(BasicMapper mapper,
|
public GetUserPortraitHandler(BasicMapper mapper,
|
||||||
@Value("${game.maimai2.userPhoto.enable:true}") boolean enable,
|
@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.mapper = mapper;
|
||||||
this.picSavePath = picSavePath;
|
this.picSavePath = picSavePath;
|
||||||
this.enable = enable;
|
this.enable = enable;
|
||||||
|
|
|
@ -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<String, Object> 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\"}";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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, Any>): 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Object> 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\"}";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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, Any>): 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue