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
|
||||
## 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
|
||||
|
|
|
@ -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<String, Any>.toUrl() = entries.joinToString("&") { (k, v) -> "$k=$v" }
|
||||
|
||||
// Map
|
||||
operator fun <K, V> Map<K, V>.plus(map: Map<K, V>) =
|
||||
(if (this is MutableMap) this else toMutableMap()).apply { 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,
|
||||
@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;
|
||||
|
|
|
@ -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