Compare commits
10 Commits
99b8127934
...
2d2c37f1bf
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d2c37f1bf | |||
| 061a85f9a1 | |||
| bc8427e6a0 | |||
| 29db5ab157 | |||
| 884f80d3c0 | |||
| a6b910ee31 | |||
| a878bf8c40 | |||
| 02cf38acda | |||
|
|
c51c2416ef | ||
| 4fddefd78d |
9
DockerFile
Normal file
9
DockerFile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM eclipse-temurin:17-jdk-jammy
|
||||
MAINTAINER TongTongStudio <tongtongstudios@gmail.com>
|
||||
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak
|
||||
VOLUME /tmp
|
||||
RUN cd /tmp
|
||||
ADD bsdiff .
|
||||
ADD target/*.jar app.jar
|
||||
EXPOSE map[65534/tcp:{}]
|
||||
ENTRYPOINT ["java", "-jar", "/app.jar"]
|
||||
1
README.md
Normal file
1
README.md
Normal file
@@ -0,0 +1 @@
|
||||
2025.05.12 android端需要增加获取Android/data目录下的所有文件,计算所有hash
|
||||
BIN
bsdiff/linux/bsdiff
Executable file
BIN
bsdiff/linux/bsdiff
Executable file
Binary file not shown.
BIN
bsdiff/linux/bspatch
Executable file
BIN
bsdiff/linux/bspatch
Executable file
Binary file not shown.
BIN
bsdiff/mac/bsdiff
Executable file
BIN
bsdiff/mac/bsdiff
Executable file
Binary file not shown.
BIN
bsdiff/mac/bspatch
Executable file
BIN
bsdiff/mac/bspatch
Executable file
Binary file not shown.
16
pom.xml
16
pom.xml
@@ -84,6 +84,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
@@ -94,6 +98,18 @@
|
||||
<artifactId>apk-parser</artifactId>
|
||||
<version>2.6.10</version>
|
||||
</dependency>
|
||||
<!--异步-->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>alibabacloud-push20160801</artifactId>
|
||||
<version>1.0.13</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>push20160801</artifactId>
|
||||
<version>1.0.17</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
10
src/main/java/com/mir4updater/backend/config/FilePath.java
Normal file
10
src/main/java/com/mir4updater/backend/config/FilePath.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.mir4updater.backend.config;
|
||||
|
||||
public class FilePath {
|
||||
|
||||
public static final String UPLOAD_PATH = "uploadApk";
|
||||
public static final String APK_FILE_PATH = "file";
|
||||
public static final String ICON_PATH = "icon";
|
||||
public static final String TEMP_PATH = "tmp";
|
||||
public static final String PATCH_PATH = "patch";
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.mir4updater.backend.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
|
||||
|
||||
@Configuration
|
||||
public class WebSocketConfig {
|
||||
@Bean
|
||||
public ServerEndpointExporter serverEndpointExporter() {
|
||||
return new ServerEndpointExporter();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.mir4updater.backend.controller;
|
||||
|
||||
import com.mir4updater.backend.result.Result;
|
||||
import com.mir4updater.backend.service.WebSocketService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -42,4 +43,10 @@ public class HelloController {
|
||||
String result = stringRedisTemplate.opsForValue().get("username");
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/web_send")
|
||||
public String webSendMessage(@RequestParam(value = "message") String message) {
|
||||
WebSocketService.sendMessageAll(message);
|
||||
return "sendMessageAll";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.mir4updater.backend.controller;
|
||||
|
||||
import com.mir4updater.backend.utils.IpUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class IpController {
|
||||
|
||||
@GetMapping("/public-ip")
|
||||
public String getPublicIp(HttpServletRequest request) {
|
||||
String clientIp = IpUtils.getClientIpAddress(request);
|
||||
return "Your public IP address is: " + clientIp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.mir4updater.backend.controller.apk;
|
||||
|
||||
import com.mir4updater.backend.entity.PatchFileInfo;
|
||||
import com.mir4updater.backend.result.Result;
|
||||
import com.mir4updater.backend.service.DiffPathService;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class CheckPatchController {
|
||||
public static Logger logger = LogManager.getLogger(CheckPatchController.class);
|
||||
|
||||
@Autowired
|
||||
DiffPathService diffPathService;
|
||||
|
||||
@GetMapping("/android/check_patch")
|
||||
public Result checkPatch(@RequestParam("pkg") String pkg, @RequestParam(value = "version_code") Long versionCode, @RequestParam(value = "md5") String oldMd5) {
|
||||
PatchFileInfo patchFileInfo = diffPathService.getPatchInfo(pkg, versionCode, oldMd5);
|
||||
if (patchFileInfo == null) {
|
||||
return Result.notFound();
|
||||
} else {
|
||||
return Result.success(patchFileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.mir4updater.backend.controller.apk;
|
||||
|
||||
import com.mir4updater.backend.entity.ApkInfo;
|
||||
import com.mir4updater.backend.result.Result;
|
||||
import com.mir4updater.backend.service.ApkInfoService;
|
||||
import com.mir4updater.backend.utils.HashUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
public class CreateDiffPatchFile {
|
||||
public static Logger logger = LogManager.getLogger(CreateDiffPatchFile.class);
|
||||
|
||||
private static final String UPLOAD_PATH = "uploadApk";
|
||||
|
||||
public static final String PATCH_PATH = "patch";
|
||||
@Autowired
|
||||
private ApkInfoService apkInfoService;
|
||||
|
||||
@PostMapping("/android/create_patch")
|
||||
public Result uploadApk(@RequestParam("old_id") int oldId, @RequestParam("new_id") int newId) throws Exception {
|
||||
String projectPath = System.getProperty("user.dir");
|
||||
String patchPath = projectPath + File.separator + UPLOAD_PATH + File.separator + PATCH_PATH + File.separator;
|
||||
File patchFileDir = new File(patchPath);
|
||||
if (!patchFileDir.exists()) {
|
||||
patchFileDir.mkdirs();
|
||||
}
|
||||
|
||||
ApkInfo oldApkInfo = apkInfoService.getApkInfo(oldId);
|
||||
ApkInfo newApkInfo = apkInfoService.getApkInfo(newId);
|
||||
if (oldApkInfo == null || newApkInfo == null) {
|
||||
return Result.notFound();
|
||||
}
|
||||
if (!Objects.equals(oldApkInfo.getPackageName(), newApkInfo.getPackageName())) {
|
||||
return Result.error("包名不一致");
|
||||
}
|
||||
if (oldApkInfo.getVersionCode() >= newApkInfo.getVersionCode()) {
|
||||
return Result.error("旧版本号不能大于等于新版本号");
|
||||
}
|
||||
File patchFile = new File(patchPath + oldApkInfo.getPackageName() + "_" + oldApkInfo.getVersionCode() + "_" + newApkInfo.getVersionCode() + ".patch");
|
||||
|
||||
// 根据操作系统选择命令
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
String command;
|
||||
if (os.contains("win")) {
|
||||
command = "bsdiff" + File.separator + "win" + File.separator + "bsdiff.exe";
|
||||
} else if (os.contains("nix") || os.contains("mac")) {
|
||||
command = "bsdiff" + File.separator + "linux" + File.separator + "bsdiff";
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported OS");
|
||||
}
|
||||
File file = new File(command);
|
||||
logger.info("bsdiff file exists = " + file.exists());
|
||||
|
||||
// 将 command 和 arg 合并成一个数组,command 作为第一个参数
|
||||
List<String> cmdArgs = new ArrayList<>();
|
||||
cmdArgs.add(command); // 第一个参数是命令本身
|
||||
cmdArgs.add(oldApkInfo.getFilePath());// 后续参数
|
||||
cmdArgs.add(newApkInfo.getFilePath());
|
||||
cmdArgs.add(patchFile.getAbsolutePath());
|
||||
|
||||
logger.info("bsdiff args = " + cmdArgs);
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(cmdArgs); // 传入合并后的参数列表
|
||||
pb.redirectErrorStream(true); // 合并错误流和输入流
|
||||
Process process = pb.start();
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
logger.info("退出码: " + exitCode);
|
||||
|
||||
// 读取输出
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
String result = reader.lines().collect(Collectors.joining("\n"));
|
||||
if (exitCode == 0) {
|
||||
String md5 = HashUtils.getFileMD5(patchFile);
|
||||
logger.info("patchFile md5 = " + md5);
|
||||
return Result.success(md5);
|
||||
} else {
|
||||
return Result.error(result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package com.mir4updater.backend.controller.apk;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mir4updater.backend.config.FilePath;
|
||||
import com.mir4updater.backend.entity.ApkInfo;
|
||||
import com.mir4updater.backend.entity.PatchFileInfo;
|
||||
import com.mir4updater.backend.entity.XapkManifest;
|
||||
import com.mir4updater.backend.result.Result;
|
||||
import com.mir4updater.backend.service.ApkInfoService;
|
||||
import com.mir4updater.backend.service.DiffPathService;
|
||||
import com.mir4updater.backend.utils.ApkUtils;
|
||||
import com.mir4updater.backend.utils.HashUtils;
|
||||
import com.mir4updater.backend.utils.PatchUtils;
|
||||
import net.dongliu.apk.parser.ApkFile;
|
||||
import net.dongliu.apk.parser.bean.ApkMeta;
|
||||
import net.dongliu.apk.parser.bean.UseFeature;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.tomcat.util.http.fileupload.FileUploadException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
@RestController
|
||||
public class CreateDiffPatchFileController {
|
||||
public static Logger logger = LogManager.getLogger(CreateDiffPatchFileController.class);
|
||||
|
||||
@Autowired
|
||||
private ApkInfoService apkInfoService;
|
||||
|
||||
@Autowired
|
||||
DiffPathService diffPathService;
|
||||
|
||||
@PostMapping("/android/create_patch")
|
||||
public Result uploadApk(@RequestParam("old_id") int oldId, @RequestParam("new_id") int newId) throws Exception {
|
||||
String projectPath = System.getProperty("user.dir");
|
||||
String patchPath = projectPath + File.separator + FilePath.UPLOAD_PATH + File.separator + FilePath.PATCH_PATH + File.separator;
|
||||
File patchFileDir = new File(patchPath);
|
||||
if (!patchFileDir.exists()) {
|
||||
patchFileDir.mkdirs();
|
||||
}
|
||||
|
||||
ApkInfo oldApkInfo = apkInfoService.getApkInfo(oldId);
|
||||
ApkInfo newApkInfo = apkInfoService.getApkInfo(newId);
|
||||
if (oldApkInfo == null || newApkInfo == null) {
|
||||
return Result.notFound();
|
||||
}
|
||||
if (!Objects.equals(oldApkInfo.getPackageName(), newApkInfo.getPackageName())) {
|
||||
return Result.error("包名不一致");
|
||||
}
|
||||
if (oldApkInfo.getVersionCode() >= newApkInfo.getVersionCode()) {
|
||||
return Result.error("旧版本号不能大于等于新版本号");
|
||||
}
|
||||
File patchFile = new File(patchPath + oldApkInfo.getPackageName() + "_" + oldApkInfo.getVersionCode() + "_" + newApkInfo.getVersionCode() + ".patch");
|
||||
long startTime = System.currentTimeMillis();
|
||||
logger.info("started at " + startTime);
|
||||
PatchUtils.BsdiffResult bsdiffResult = PatchUtils.createDiffPatch(oldApkInfo.getFilePath(), newApkInfo.getFilePath(), patchFile.getPath());
|
||||
long endTime = System.currentTimeMillis() - startTime;
|
||||
int exitCode = bsdiffResult.getExitCode();
|
||||
String result = bsdiffResult.getResult();
|
||||
|
||||
if (exitCode == 0) {
|
||||
String md5 = HashUtils.getFileMD5(patchFile);
|
||||
logger.info("patchFile md5 = " + md5 + " 用时:" + endTime + "ms");
|
||||
return Result.success(md5);
|
||||
} else {
|
||||
return Result.error(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过上传文件查询最新版本并创建最新补丁文件
|
||||
*
|
||||
* @param multipartFile
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@PostMapping("/android/create_latest_patch")
|
||||
public Result createLatestPatch(@RequestParam("file") MultipartFile multipartFile) throws Exception {
|
||||
if (multipartFile != null && !multipartFile.isEmpty()) {
|
||||
String projectPath = System.getProperty("user.dir");
|
||||
logger.info("当前项目路径为:" + projectPath);
|
||||
String tempPath = projectPath + File.separator + FilePath.UPLOAD_PATH + File.separator + FilePath.TEMP_PATH + File.separator;
|
||||
File tempFileDir = new File(tempPath);
|
||||
if (!tempFileDir.exists()) {
|
||||
tempFileDir.mkdirs();
|
||||
}
|
||||
//1.接收上传的应用,写入到硬盘
|
||||
String originalFilename = multipartFile.getOriginalFilename();
|
||||
File tempFile = new File(tempPath + File.separator + originalFilename);
|
||||
logger.info("tempFile path = " + tempFile.getAbsolutePath());
|
||||
|
||||
try {
|
||||
multipartFile.transferTo(tempFile);
|
||||
} catch (FileUploadException e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
if (!ApkUtils.isAPK(tempFile) && !ApkUtils.isXAPK(tempFile)) {
|
||||
return Result.error().setMessage("请上传apk或xapk文件");
|
||||
}
|
||||
|
||||
String md5 = HashUtils.getFileMD5(tempFile);
|
||||
logger.info("tempFile md5 = " + md5);
|
||||
String sha1 = HashUtils.calculateSHA1(tempFile);
|
||||
logger.info("tempFile sha1 = " + sha1);
|
||||
String sha256 = HashUtils.calculateSHA256(tempFile);
|
||||
logger.info("tempFile sha256 = " + sha256);
|
||||
|
||||
|
||||
if (ApkUtils.isAPK(tempFile)) {
|
||||
logger.info("tempFile type is apk");
|
||||
|
||||
ApkFile apkFile = new ApkFile(tempFile);
|
||||
ApkMeta apkMeta = apkFile.getApkMeta();
|
||||
apkFile.close();
|
||||
|
||||
logger.info(apkMeta.getLabel());
|
||||
logger.info(apkMeta.getPackageName());
|
||||
logger.info(apkMeta.getVersionCode());
|
||||
logger.info(apkMeta.getVersionName());
|
||||
for (UseFeature feature : apkMeta.getUsesFeatures()) {
|
||||
logger.info(feature.getName());
|
||||
}
|
||||
|
||||
String packageName = apkMeta.getPackageName();
|
||||
long versionCode = apkMeta.getVersionCode();
|
||||
PatchFileInfo patchFileInfo = diffPathService.getPatchInfo(packageName, versionCode, md5);
|
||||
if (patchFileInfo != null) {
|
||||
return Result.error("patch文件存在,请直接下载");
|
||||
}
|
||||
|
||||
List<ApkInfo> apkInfoList = apkInfoService.getApkInfo(packageName);
|
||||
Optional<ApkInfo> apkInfoOptional = apkInfoList.stream().max(new Comparator<ApkInfo>() {
|
||||
@Override
|
||||
public int compare(ApkInfo o1, ApkInfo o2) {
|
||||
return Long.compare(o1.getVersionCode(), o2.getVersionCode());
|
||||
}
|
||||
});
|
||||
if (apkInfoOptional.isPresent()) {
|
||||
ApkInfo apkInfo = apkInfoOptional.get();
|
||||
logger.info("latest apkinfo = " + apkInfo);
|
||||
if (versionCode >= apkInfo.getVersionCode()) {
|
||||
return Result.success().setMessage(packageName + " 已是最新版本");
|
||||
} else {
|
||||
String apkPath = apkInfo.getFilePath();
|
||||
String patchPath = projectPath + File.separator + FilePath.UPLOAD_PATH + File.separator + FilePath.PATCH_PATH + File.separator;
|
||||
File patchFile = new File(patchPath + packageName + "_" + versionCode + "_" + apkInfo.getVersionCode() + ".patch");
|
||||
long startTime = System.currentTimeMillis();
|
||||
logger.info("started at " + startTime);
|
||||
PatchUtils.BsdiffResult bsdiffResult = PatchUtils.createDiffPatch(tempFile.getAbsolutePath(), apkPath, patchFile.getPath());
|
||||
long endTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
int exitCode = bsdiffResult.getExitCode();
|
||||
String result = bsdiffResult.getResult();
|
||||
|
||||
if (exitCode == 0) {
|
||||
String patchMd5 = HashUtils.getFileMD5(patchFile);
|
||||
logger.info("patchFile md5 = " + patchMd5);
|
||||
logger.info("tempFile delete = " + tempFile.delete());
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("patchMd5", patchMd5);
|
||||
map.put("path", patchFile.getAbsolutePath());
|
||||
map.put("time", endTime + "ms");
|
||||
return Result.mapResult(map).setSuccess(true).setCode(Result.CODE_SUCCESS).setMessage("创建成功");
|
||||
} else {
|
||||
logger.info("tempFile delete = " + tempFile.delete());
|
||||
return Result.error(result);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info("tempFile delete = " + tempFile.delete());
|
||||
return Result.error().setMessage(packageName + " 没有上传其他版本");
|
||||
}
|
||||
} else if (ApkUtils.isXAPK(tempFile)) {
|
||||
//一般不会出现这种情况
|
||||
logger.info("tempFile type is xapk");
|
||||
try (ZipFile zipFile = new ZipFile(tempFile)) {
|
||||
// 遍历ZIP文件中的所有条目
|
||||
zipFile.stream().forEach(zipEntry -> {
|
||||
logger.info(zipEntry.getName());
|
||||
});
|
||||
Optional<? extends ZipEntry> manifestOptional = zipFile.stream().filter(zipEntry -> "manifest.json".equals(zipEntry.getName())).findFirst();
|
||||
if (manifestOptional.isPresent()) {
|
||||
ZipEntry manifestZipEntry = manifestOptional.get();
|
||||
try {
|
||||
InputStream manifestInputStream = zipFile.getInputStream(manifestZipEntry);
|
||||
// 读取文件内容
|
||||
String content = new String(manifestInputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
// 创建ObjectMapper实例
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
// 将Java对象转换为JSON字符串
|
||||
String jsonString = objectMapper.writeValueAsString(content);
|
||||
XapkManifest xapkManifest = objectMapper.readValue(content, XapkManifest.class);
|
||||
// logger.info(xapkManifest);
|
||||
logger.info(xapkManifest.getName());
|
||||
logger.info(xapkManifest.getPackage_name());
|
||||
logger.info(xapkManifest.getVersion_code());
|
||||
logger.info(xapkManifest.getVersion_name());
|
||||
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("读取文件失败: " + manifestZipEntry.getName());
|
||||
return Result.error().setMessage("读取文件失败");
|
||||
}
|
||||
} else {
|
||||
return Result.error().setMessage("读取xapk配置失败");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("打开ZIP文件失败: " + e.getMessage());
|
||||
return Result.error().setMessage("打开ZIP文件失败");
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
return Result.error().setMessage("文件为空");
|
||||
}
|
||||
return Result.error().setMessage("未知错误");
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.mir4updater.backend.controller.apk;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mir4updater.backend.config.FilePath;
|
||||
import com.mir4updater.backend.entity.ApkInfo;
|
||||
import com.mir4updater.backend.entity.XapkManifest;
|
||||
import com.mir4updater.backend.result.Result;
|
||||
import com.mir4updater.backend.service.ApkInfoService;
|
||||
import com.mir4updater.backend.utils.ApkUtils;
|
||||
import com.mir4updater.backend.utils.FileUtils;
|
||||
import com.mir4updater.backend.utils.HashUtils;
|
||||
import net.dongliu.apk.parser.ApkFile;
|
||||
@@ -33,10 +35,7 @@ import java.util.zip.ZipFile;
|
||||
|
||||
@RestController
|
||||
public class UploadApkController {
|
||||
private static final String UPLOAD_PATH = "uploadApk";
|
||||
private static final String APK_FILE_PATH = "file";
|
||||
private static final String ICON_PATH = "icon";
|
||||
private static final String TEMP_PATH = "tmp";
|
||||
|
||||
|
||||
public static Logger logger = LogManager.getLogger(UploadApkController.class);
|
||||
|
||||
@@ -50,9 +49,9 @@ public class UploadApkController {
|
||||
String projectPath = System.getProperty("user.dir");
|
||||
logger.info("当前项目路径为:" + projectPath);
|
||||
|
||||
String apkPath = projectPath + File.separator + UPLOAD_PATH + File.separator + APK_FILE_PATH;
|
||||
String iconPath = projectPath + File.separator + UPLOAD_PATH + File.separator + ICON_PATH + File.separator;
|
||||
String tempPath = projectPath + File.separator + UPLOAD_PATH + File.separator + TEMP_PATH + File.separator;
|
||||
String apkPath = projectPath + File.separator + FilePath.UPLOAD_PATH + File.separator + FilePath.APK_FILE_PATH;
|
||||
String iconPath = projectPath + File.separator + FilePath.UPLOAD_PATH + File.separator + FilePath.ICON_PATH + File.separator;
|
||||
String tempPath = projectPath + File.separator + FilePath.UPLOAD_PATH + File.separator + FilePath.TEMP_PATH + File.separator;
|
||||
|
||||
File apkFileDir = new File(apkPath);
|
||||
if (!apkFileDir.exists()) {
|
||||
@@ -78,7 +77,7 @@ public class UploadApkController {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
if (!isAPK(file) && !isXAPK(file)) {
|
||||
if (!ApkUtils.isAPK(file) && !ApkUtils.isXAPK(file)) {
|
||||
return Result.error().setMessage("请上传apk或xapk文件");
|
||||
}
|
||||
String md5 = HashUtils.getFileMD5(file);
|
||||
@@ -90,7 +89,7 @@ public class UploadApkController {
|
||||
|
||||
File iconFile = new File(iconPath + md5 + ".png");
|
||||
|
||||
if (isAPK(file)) {
|
||||
if (ApkUtils.isAPK(file)) {
|
||||
logger.info("file type is apk");
|
||||
//调用工具类解析apk
|
||||
//analysisAPK(apkFileDir.getAbsolutePath(), file.getAbsolutePath());
|
||||
@@ -135,7 +134,7 @@ public class UploadApkController {
|
||||
}
|
||||
|
||||
return Result.success().setMessage("上传成功");
|
||||
} else if (isXAPK(file)) {
|
||||
} else if (ApkUtils.isXAPK(file)) {
|
||||
logger.info("file type is xapk");
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
// 遍历ZIP文件中的所有条目
|
||||
@@ -210,54 +209,6 @@ public class UploadApkController {
|
||||
return Result.error().setMessage("未知错误");
|
||||
}
|
||||
|
||||
// 判断是否为APK
|
||||
public static boolean isAPK(File file) throws IOException {
|
||||
// 检查扩展名是否为.apk
|
||||
if (file.getName().toLowerCase().endsWith(".apk")) {
|
||||
// 进一步验证是否为有效的APK文件
|
||||
ZipFile zipFile = new ZipFile(file);
|
||||
boolean apkFile = zipFile.getEntry("AndroidManifest.xml") != null;
|
||||
zipFile.close();
|
||||
return apkFile;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断是否为XAPK
|
||||
public static boolean isXAPK(File file) {
|
||||
// 检查扩展名是否为.xapk
|
||||
if (file.getName().toLowerCase().endsWith(".xapk")) {
|
||||
return true; // 扩展名为.xapk通常直接视为XAPK
|
||||
}
|
||||
|
||||
// 若扩展名不明确,检查内容结构
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
boolean hasJson = zipFile.getEntry("manifest.json") != null;
|
||||
|
||||
boolean hasAPK = false;
|
||||
// boolean hasOBB = false;
|
||||
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
// 检查是否包含APK文件
|
||||
if (name.endsWith(".apk")) {
|
||||
hasAPK = true;
|
||||
}
|
||||
// 检查是否包含OBB目录
|
||||
// if (name.startsWith("Android/obb/")) {
|
||||
// hasOBB = true;
|
||||
// }
|
||||
}
|
||||
zipFile.close();
|
||||
// XAPK通常包含至少一个APK或OBB数据
|
||||
return hasAPK || hasJson;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void analysisAPK(String dirPath, String filePath) throws Exception {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.mir4updater.backend.controller.push;
|
||||
|
||||
import com.aliyun.auth.credentials.Credential;
|
||||
import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
|
||||
import com.aliyun.sdk.service.push20160801.AsyncClient;
|
||||
import com.aliyun.sdk.service.push20160801.models.BindAliasRequest;
|
||||
import com.aliyun.sdk.service.push20160801.models.BindAliasResponse;
|
||||
import com.google.gson.Gson;
|
||||
import darabonba.core.client.ClientOverrideConfiguration;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class AsyncPush {
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
// HttpClient Configuration
|
||||
/*HttpClient httpClient = new ApacheAsyncHttpClientBuilder()
|
||||
.connectionTimeout(Duration.ofSeconds(10)) // Set the connection timeout time, the default is 10 seconds
|
||||
.responseTimeout(Duration.ofSeconds(10)) // Set the response timeout time, the default is 20 seconds
|
||||
.maxConnections(128) // Set the connection pool size
|
||||
.maxIdleTimeOut(Duration.ofSeconds(50)) // Set the connection pool timeout, the default is 30 seconds
|
||||
// Configure the proxy
|
||||
.proxy(new ProxyOptions(ProxyOptions.Type.HTTP, new InetSocketAddress("<your-proxy-hostname>", 9001))
|
||||
.setCredentials("<your-proxy-username>", "<your-proxy-password>"))
|
||||
// If it is an https connection, you need to configure the certificate, or ignore the certificate(.ignoreSSL(true))
|
||||
.x509TrustManagers(new X509TrustManager[]{})
|
||||
.keyManagers(new KeyManager[]{})
|
||||
.ignoreSSL(false)
|
||||
.build();*/
|
||||
|
||||
// Configure Credentials authentication information, including ak, secret, token
|
||||
StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
|
||||
// Please ensure that the environment variables ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET are set.
|
||||
.accessKeyId("335514186")
|
||||
.accessKeySecret("dc39560b76c54c408ece7dce2f21464f")
|
||||
//.securityToken(System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN")) // use STS token
|
||||
.build());
|
||||
|
||||
// Configure the Client
|
||||
AsyncClient client = AsyncClient.builder()
|
||||
//.httpClient(httpClient) // Use the configured HttpClient, otherwise use the default HttpClient (Apache HttpClient)
|
||||
.credentialsProvider(provider)
|
||||
//.serviceConfiguration(Configuration.create()) // Service-level configuration
|
||||
// Client-level configuration rewrite, can set Endpoint, Http request parameters, etc.
|
||||
.overrideConfiguration(
|
||||
ClientOverrideConfiguration.create()
|
||||
// Endpoint 请参考 https://api.aliyun.com/product/Push
|
||||
.setEndpointOverride("cloudpush.aliyuncs.com")
|
||||
//.setConnectTimeout(Duration.ofSeconds(30))
|
||||
)
|
||||
.build();
|
||||
|
||||
// Parameter settings for API request
|
||||
BindAliasRequest bindAliasRequest = BindAliasRequest.builder()
|
||||
.appKey(1L)
|
||||
.deviceId("e0b4fb8ebeb44de5be3617f4a901b8f5")
|
||||
// Request-level configuration rewrite, can set Http request parameters, etc.
|
||||
// .requestConfiguration(RequestConfiguration.create().setHttpHeaders(new HttpHeaders()))
|
||||
.build();
|
||||
|
||||
// Asynchronously get the return value of the API request
|
||||
CompletableFuture<BindAliasResponse> response = client.bindAlias(bindAliasRequest);
|
||||
// Synchronously get the return value of the API request
|
||||
BindAliasResponse resp = response.get();
|
||||
System.out.println(new Gson().toJson(resp));
|
||||
// Asynchronous processing of return values
|
||||
/*response.thenAccept(resp -> {
|
||||
System.out.println(new Gson().toJson(resp));
|
||||
}).exceptionally(throwable -> { // Handling exceptions
|
||||
System.out.println(throwable.getMessage());
|
||||
return null;
|
||||
});*/
|
||||
|
||||
// Finally, close the client
|
||||
client.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.mir4updater.backend.controller.push;
|
||||
|
||||
import com.aliyun.push20160801.models.PushRequest;
|
||||
import com.aliyun.push20160801.models.PushResponse;
|
||||
import com.aliyun.teaopenapi.models.Config;
|
||||
|
||||
public class SyncPush {
|
||||
|
||||
public static com.aliyun.push20160801.Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
|
||||
Config config = new Config();
|
||||
// 您的AccessKey ID
|
||||
config.accessKeyId = accessKeyId;
|
||||
// 您的AccessKey Secret
|
||||
config.accessKeySecret = accessKeySecret;
|
||||
config.regionId = "cn-hangzhou";
|
||||
return new com.aliyun.push20160801.Client(config);
|
||||
}
|
||||
|
||||
public static void main(String[] args_) throws Exception {
|
||||
com.aliyun.push20160801.Client client = createClient("LTAI5tBWFbkuKgcobdcqpFus",
|
||||
"bYvEk0lgfvUzuA8NSlcSTKPkKy5Uow");
|
||||
PushRequest request = new PushRequest()
|
||||
.setAppKey(335514186L)
|
||||
.setPushType("MESSAGE")
|
||||
.setDeviceType("ANDROID")
|
||||
.setStoreOffline(true)
|
||||
.setIOSRemind(true)
|
||||
.setAndroidRemind(true)
|
||||
.setTarget("ALIAS")
|
||||
.setTargetValue("e0b4fb8ebeb44de5be3617f4a901b8f5")
|
||||
.setTitle("test")
|
||||
.setBody("2")
|
||||
.setIOSRemindBody("3")
|
||||
.setAndroidPopupTitle("4")
|
||||
.setAndroidPopupBody("5");
|
||||
PushResponse response = client.push(request);
|
||||
System.out.println(response.getStatusCode());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.mir4updater.backend.entity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@@ -10,8 +12,9 @@ import java.io.Serializable;
|
||||
@Entity
|
||||
@Table(name = "apk_info")
|
||||
@EqualsAndHashCode
|
||||
|
||||
public class ApkInfo implements Serializable {
|
||||
public ApkInfo() {
|
||||
}
|
||||
|
||||
@Embedded
|
||||
private ApkInfoPK id; // 复合主键
|
||||
@@ -51,86 +54,6 @@ public class ApkInfo implements Serializable {
|
||||
@Column(name = "xapk_version", length = 11)
|
||||
int xapk_version;
|
||||
|
||||
public ApkInfo() {
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return appName;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public long getVersionCode() {
|
||||
return versionCode;
|
||||
}
|
||||
|
||||
public void setVersionCode(long versionCode) {
|
||||
this.versionCode = versionCode;
|
||||
}
|
||||
|
||||
public String getVersionName() {
|
||||
return versionName;
|
||||
}
|
||||
|
||||
public void setVersionName(String versionName) {
|
||||
this.versionName = versionName;
|
||||
}
|
||||
|
||||
public String getSha1() {
|
||||
return sha1;
|
||||
}
|
||||
|
||||
public void setSha1(String sha1) {
|
||||
this.sha1 = sha1;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getIconPath() {
|
||||
return iconPath;
|
||||
}
|
||||
|
||||
public void setIconPath(String iconPath) {
|
||||
this.iconPath = iconPath;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public boolean isXapk() {
|
||||
return xapk;
|
||||
}
|
||||
|
||||
public void setXapk(boolean xapk) {
|
||||
this.xapk = xapk;
|
||||
}
|
||||
|
||||
public int getXapk_version() {
|
||||
return xapk_version;
|
||||
}
|
||||
|
||||
public void setXapk_version(int xapk_version) {
|
||||
this.xapk_version = xapk_version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.mir4updater.backend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "patch_file_info")
|
||||
@EqualsAndHashCode
|
||||
public class PatchFileInfo implements Serializable {
|
||||
@Id
|
||||
@Column(name = "patch_id", nullable = false, length = 20)
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY) // 主键自增
|
||||
int appId;
|
||||
|
||||
@Column(name = "package_name", nullable = false)
|
||||
String packageName;
|
||||
|
||||
@Column(name = "old_version", nullable = false)
|
||||
long oldVersion;
|
||||
|
||||
@Column(name = "old_md5", nullable = false)
|
||||
String oldMd5;
|
||||
|
||||
@Column(name = "old_sha1", nullable = false)
|
||||
String oldSha1;
|
||||
|
||||
@Column(name = "old_sha256", nullable = false)
|
||||
String oldSha256;
|
||||
|
||||
@Column(name = "new_version", nullable = false)
|
||||
long newVersion;
|
||||
|
||||
@Column(name = "new_md5", nullable = false)
|
||||
String newMd5;
|
||||
|
||||
@Column(name = "new_sha1", nullable = false)
|
||||
String newSha1;
|
||||
|
||||
@Column(name = "new_sha256", nullable = false)
|
||||
String newSha256;
|
||||
|
||||
@Column(name = "patch_file_path", nullable = false)
|
||||
String patch_file_path;
|
||||
|
||||
@Column(name = "patch_file_md5", nullable = false)
|
||||
String patch_file_md5;
|
||||
|
||||
@Column(name = "patch_file_sha1", nullable = false)
|
||||
String patch_file_sha1;
|
||||
|
||||
@Column(name = "patch_file_sha256", nullable = false)
|
||||
String patch_file_sha256;
|
||||
|
||||
@Column(name = "new_file_name", nullable = false)
|
||||
String new_file_name;
|
||||
|
||||
@Column(name = "is_xapk", columnDefinition = "bit(1) default 0")
|
||||
boolean xapk = false;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.mir4updater.backend.repository;
|
||||
|
||||
import com.mir4updater.backend.entity.PatchFileInfo;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PatchInfoRepository extends JpaRepository<PatchFileInfo, Long> {
|
||||
public PatchFileInfo findPatchFileInfoByPackageNameAndOldVersionAndOldMd5(String packageName, Long oldVersion, String oldMd5);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.mir4updater.backend.service;
|
||||
|
||||
import com.mir4updater.backend.entity.PatchFileInfo;
|
||||
import com.mir4updater.backend.repository.PatchInfoRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DiffPathService {
|
||||
@Autowired
|
||||
PatchInfoRepository patchInfoRepository;
|
||||
|
||||
public PatchFileInfo getPatchInfo(String packageName, Long oldVersion, String oldMd5) {
|
||||
return patchInfoRepository.findPatchFileInfoByPackageNameAndOldVersionAndOldMd5(packageName, oldVersion, oldMd5);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.mir4updater.backend.service;
|
||||
|
||||
import com.aliyun.push20160801.models.PushRequest;
|
||||
import com.aliyun.push20160801.models.PushResponse;
|
||||
import com.aliyun.teaopenapi.models.Config;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class PushService {
|
||||
|
||||
public static com.aliyun.push20160801.Client createClient() throws Exception {
|
||||
Config config = new Config();
|
||||
// 您的AccessKey ID
|
||||
config.accessKeyId = "LTAI5tBWFbkuKgcobdcqpFus";
|
||||
// 您的AccessKey Secret
|
||||
config.accessKeySecret = "bYvEk0lgfvUzuA8NSlcSTKPkKy5Uow";
|
||||
config.regionId = "cn-shenzhen";
|
||||
return new com.aliyun.push20160801.Client(config);
|
||||
}
|
||||
|
||||
public PushResponse pushUpdate(String uuid, String message) throws Exception {
|
||||
PushRequest request = new PushRequest()
|
||||
.setAppKey(335514186L)
|
||||
.setPushType("MESSAGE")
|
||||
.setDeviceType("ANDROID")
|
||||
.setTarget("ALIAS")
|
||||
.setTargetValue(uuid)
|
||||
.setTitle("test")
|
||||
.setBody("2");
|
||||
PushResponse response = createClient().push(request);
|
||||
return response;
|
||||
}
|
||||
|
||||
public PushResponse pushAllDevices(String message) throws Exception {
|
||||
PushRequest request = new PushRequest()
|
||||
.setAppKey(335514186L)
|
||||
.setPushType("MESSAGE")
|
||||
.setDeviceType("ANDROID")
|
||||
.setTarget("ALL")
|
||||
.setTargetValue("ALL")
|
||||
.setTitle("test")
|
||||
.setBody("2");
|
||||
PushResponse response = createClient().push(request);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.mir4updater.backend.service;
|
||||
|
||||
import jakarta.websocket.OnClose;
|
||||
import jakarta.websocket.OnMessage;
|
||||
import jakarta.websocket.OnOpen;
|
||||
import jakarta.websocket.Session;
|
||||
import jakarta.websocket.server.PathParam;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@ServerEndpoint("/websocket/{terminalId}")
|
||||
public class WebSocketService {
|
||||
private static final Map<String, Session> CLIENTS = new ConcurrentHashMap<>();
|
||||
|
||||
public static Logger logger = LogManager.getLogger(WebSocketService.class);
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(@PathParam("terminalId") String terminalId, Session session) {
|
||||
if (CLIENTS.containsKey(terminalId)) {
|
||||
try {
|
||||
CLIENTS.get(terminalId).close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
CLIENTS.put(terminalId, session);
|
||||
logger.info(session.getId());
|
||||
logger.info("终端 {} 已连接,当前在线数:{}", terminalId, CLIENTS.size());
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(@PathParam("terminalId") String terminalId, Session session) {
|
||||
CLIENTS.remove(terminalId);
|
||||
logger.info(session.getId());
|
||||
logger.info("终端 {} 已断开", terminalId);
|
||||
}
|
||||
|
||||
@OnMessage
|
||||
public void onMessage(String message, Session session) {
|
||||
logger.info(session.getId());
|
||||
logger.info("收到消息:{}", message);
|
||||
}
|
||||
|
||||
// 推送消息方法
|
||||
public static void sendMessage(String terminalId, String message) {
|
||||
Session session = CLIENTS.get(terminalId);
|
||||
if (session != null && session.isOpen()) {
|
||||
session.getAsyncRemote().sendText(message); // 异步发送避免阻塞
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendMessageAll(String message) {
|
||||
for (Map.Entry<String, Session> entry : CLIENTS.entrySet()) {
|
||||
Session session = entry.getValue();
|
||||
if (session != null && session.isOpen()) {
|
||||
session.getAsyncRemote().sendText(message); // 异步发送避免阻塞
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/main/java/com/mir4updater/backend/utils/ApkUtils.java
Normal file
60
src/main/java/com/mir4updater/backend/utils/ApkUtils.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package com.mir4updater.backend.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ApkUtils {
|
||||
|
||||
// 判断是否为APK
|
||||
public static boolean isAPK(File file) throws IOException {
|
||||
// 检查扩展名是否为.apk
|
||||
if (file.getName().toLowerCase().endsWith(".apk")) {
|
||||
// 进一步验证是否为有效的APK文件
|
||||
ZipFile zipFile = new ZipFile(file);
|
||||
boolean apkFile = zipFile.getEntry("AndroidManifest.xml") != null;
|
||||
zipFile.close();
|
||||
return apkFile;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断是否为XAPK
|
||||
public static boolean isXAPK(File file) {
|
||||
// 检查扩展名是否为.xapk
|
||||
if (file.getName().toLowerCase().endsWith(".xapk")) {
|
||||
return true; // 扩展名为.xapk通常直接视为XAPK
|
||||
}
|
||||
|
||||
// 若扩展名不明确,检查内容结构
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
boolean hasJson = zipFile.getEntry("manifest.json") != null;
|
||||
|
||||
boolean hasAPK = false;
|
||||
// boolean hasOBB = false;
|
||||
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
// 检查是否包含APK文件
|
||||
if (name.endsWith(".apk")) {
|
||||
hasAPK = true;
|
||||
}
|
||||
// 检查是否包含OBB目录
|
||||
// if (name.startsWith("Android/obb/")) {
|
||||
// hasOBB = true;
|
||||
// }
|
||||
}
|
||||
zipFile.close();
|
||||
// XAPK通常包含至少一个APK或OBB数据
|
||||
return hasAPK || hasJson;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
44
src/main/java/com/mir4updater/backend/utils/IpUtils.java
Normal file
44
src/main/java/com/mir4updater/backend/utils/IpUtils.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.mir4updater.backend.utils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public class IpUtils {
|
||||
public static String getClientIpAddress(HttpServletRequest request) {
|
||||
String[] headersToCheck = {
|
||||
"X-Forwarded-For",
|
||||
"Proxy-Client-IP",
|
||||
"WL-Proxy-Client-IP",
|
||||
"HTTP_CLIENT_IP",
|
||||
"HTTP_X_FORWARDED_FOR"
|
||||
};
|
||||
|
||||
String ip = null;
|
||||
for (String header : headersToCheck) {
|
||||
ip = request.getHeader(header);
|
||||
if (isValidIp(ip)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理多IP情况
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
|
||||
// 回退到默认方法
|
||||
if (!isValidIp(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
// 处理IPv6本地地址
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip)) {
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
private static boolean isValidIp(String ip) {
|
||||
return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip);
|
||||
}
|
||||
}
|
||||
82
src/main/java/com/mir4updater/backend/utils/PatchUtils.java
Normal file
82
src/main/java/com/mir4updater/backend/utils/PatchUtils.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package com.mir4updater.backend.utils;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PatchUtils {
|
||||
public static Logger logger = LogManager.getLogger(PatchUtils.class);
|
||||
|
||||
public static BsdiffResult createDiffPatch(String oldApkPath, String newApkPath, String patchFilePath) throws Exception {
|
||||
// 根据操作系统选择命令
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
String command;
|
||||
if (os.contains("win")) {
|
||||
command = "bsdiff" + File.separator + "win" + File.separator + "bsdiff.exe";
|
||||
} else if (os.contains("linux")) {
|
||||
command = "bsdiff" + File.separator + "linux" + File.separator + "bsdiff";
|
||||
} else if (os.contains("mac")) {
|
||||
command = "bsdiff" + File.separator + "mac" + File.separator + "bsdiff";
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported OS");
|
||||
}
|
||||
File file = new File(command);
|
||||
logger.info("bsdiff file exists = " + file.exists());
|
||||
|
||||
// 将 command 和 arg 合并成一个数组,command 作为第一个参数
|
||||
List<String> cmdArgs = new ArrayList<>();
|
||||
cmdArgs.add(command); // 第一个参数是命令本身
|
||||
cmdArgs.add(oldApkPath);// 后续参数
|
||||
cmdArgs.add(newApkPath);
|
||||
cmdArgs.add(patchFilePath);
|
||||
|
||||
logger.info("bsdiff args = " + cmdArgs);
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(cmdArgs); // 传入合并后的参数列表
|
||||
pb.redirectErrorStream(true); // 合并错误流和输入流
|
||||
Process process = pb.start();
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
logger.info("退出码: " + exitCode);
|
||||
|
||||
// 读取输出
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
String result = reader.lines().collect(Collectors.joining("\n"));
|
||||
BsdiffResult bsdiffResult = new BsdiffResult(exitCode, result);
|
||||
|
||||
return bsdiffResult;
|
||||
}
|
||||
|
||||
public static class BsdiffResult implements Serializable {
|
||||
int exitCode;
|
||||
String result;
|
||||
|
||||
public BsdiffResult(int exitCode, String result) {
|
||||
this.exitCode = exitCode;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public int getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public void setExitCode(int exitCode) {
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(String result) {
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/main/resources/application-debug.properties
Normal file
36
src/main/resources/application-debug.properties
Normal file
@@ -0,0 +1,36 @@
|
||||
spring.application.name=Mir4Updater
|
||||
server.port=65533
|
||||
server.address=0.0.0.0
|
||||
## \u6570\u636E\u8FDE\u63A5\u4FE1\u606F
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.url=jdbc:mysql://127.0.0.1:3305/spring_boot?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
spring.datasource.username=tt
|
||||
spring.datasource.password=fanhuitong
|
||||
# Hibernate\u914D\u7F6E
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
|
||||
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
|
||||
# \u542F\u7528 SQL \u65E5\u5FD7
|
||||
spring.jpa.show-sql=true
|
||||
# \u683C\u5F0F\u5316 SQL
|
||||
#spring.jpa.properties.hibernate.format_sql=true
|
||||
# \u663E\u793A\u7ED1\u5B9A\u53C2\u6570\u7684\u5177\u4F53\u503C\uFF08\u5173\u952E\u914D\u7F6E\uFF09
|
||||
logging.level.org.hibernate.type.descriptor.sql=TRACE
|
||||
# \u6587\u4EF6\u5927\u5C0F
|
||||
spring.servlet.multipart.max-file-size=2GB
|
||||
spring.servlet.multipart.max-request-size=2GB
|
||||
mybatis.type-aliases-package=com.mir4updater.backend.entity
|
||||
mybatis.mapperLocations=classpath:mapper/*.xml
|
||||
# redis\u57FA\u7840\u914D\u7F6E
|
||||
# 0\u4E5F\u662F\u9ED8\u8BA4\u503C\uFF0C\u8868\u793A\u4F60\u8981\u64CD\u63A7\u7684 Redis \u4E0A\u7684\u54EA\u4E2A\u6570\u636E\u5E93
|
||||
spring.data.redis.database=0
|
||||
# 6379\u4E5F\u662F\u9ED8\u8BA4\u503C\uFF0C\u8868\u793A Redis \u7AEF\u53E3
|
||||
spring.data.redis.port=6379
|
||||
# \u8FD9\u91CC\u586B\u5199\u4F60\u7684\u670D\u52A1\u5668\u5730\u5740
|
||||
spring.data.redis.host=127.0.0.1
|
||||
spring.data.redis.password=fanhuitong
|
||||
# \u53EF\u7701\u7565
|
||||
spring.data.redis.lettuce.pool.min-idle=5
|
||||
spring.data.redis.lettuce.pool.max-idle=10
|
||||
spring.data.redis.lettuce.pool.max-active=8
|
||||
spring.data.redis.lettuce.pool.max-wait=1ms
|
||||
spring.data.redis.lettuce.shutdown-timeout=100ms
|
||||
36
src/main/resources/application-prod.properties
Normal file
36
src/main/resources/application-prod.properties
Normal file
@@ -0,0 +1,36 @@
|
||||
spring.application.name=Mir4Updater
|
||||
server.port=65534
|
||||
server.address=0.0.0.0
|
||||
## \u6570\u636E\u8FDE\u63A5\u4FE1\u606F
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.url=jdbc:mysql://127.0.0.1:3305/spring_boot?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
spring.datasource.username=tt
|
||||
spring.datasource.password=fanhuitong
|
||||
# Hibernate\u914D\u7F6E
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
|
||||
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
|
||||
# \u542F\u7528 SQL \u65E5\u5FD7
|
||||
spring.jpa.show-sql=true
|
||||
# \u683C\u5F0F\u5316 SQL
|
||||
#spring.jpa.properties.hibernate.format_sql=true
|
||||
# \u663E\u793A\u7ED1\u5B9A\u53C2\u6570\u7684\u5177\u4F53\u503C\uFF08\u5173\u952E\u914D\u7F6E\uFF09
|
||||
logging.level.org.hibernate.type.descriptor.sql=TRACE
|
||||
# \u6587\u4EF6\u5927\u5C0F
|
||||
spring.servlet.multipart.max-file-size=2GB
|
||||
spring.servlet.multipart.max-request-size=2GB
|
||||
mybatis.type-aliases-package=com.mir4updater.backend.entity
|
||||
mybatis.mapperLocations=classpath:mapper/*.xml
|
||||
# redis\u57FA\u7840\u914D\u7F6E
|
||||
# 0\u4E5F\u662F\u9ED8\u8BA4\u503C\uFF0C\u8868\u793A\u4F60\u8981\u64CD\u63A7\u7684 Redis \u4E0A\u7684\u54EA\u4E2A\u6570\u636E\u5E93
|
||||
spring.data.redis.database=0
|
||||
# 6379\u4E5F\u662F\u9ED8\u8BA4\u503C\uFF0C\u8868\u793A Redis \u7AEF\u53E3
|
||||
spring.data.redis.port=6379
|
||||
# \u8FD9\u91CC\u586B\u5199\u4F60\u7684\u670D\u52A1\u5668\u5730\u5740
|
||||
spring.data.redis.host=127.0.0.1
|
||||
spring.data.redis.password=fanhuitong
|
||||
# \u53EF\u7701\u7565
|
||||
spring.data.redis.lettuce.pool.min-idle=5
|
||||
spring.data.redis.lettuce.pool.max-idle=10
|
||||
spring.data.redis.lettuce.pool.max-active=8
|
||||
spring.data.redis.lettuce.pool.max-wait=1ms
|
||||
spring.data.redis.lettuce.shutdown-timeout=100ms
|
||||
36
src/main/resources/application-test.properties
Normal file
36
src/main/resources/application-test.properties
Normal file
@@ -0,0 +1,36 @@
|
||||
spring.application.name=Mir4Updater
|
||||
server.port=65532
|
||||
server.address=0.0.0.0
|
||||
## \u6570\u636E\u8FDE\u63A5\u4FE1\u606F
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.url=jdbc:mysql://127.0.0.1:3305/spring_boot?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
spring.datasource.username=tt
|
||||
spring.datasource.password=fanhuitong
|
||||
# Hibernate\u914D\u7F6E
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
|
||||
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
|
||||
# \u542F\u7528 SQL \u65E5\u5FD7
|
||||
spring.jpa.show-sql=true
|
||||
# \u683C\u5F0F\u5316 SQL
|
||||
#spring.jpa.properties.hibernate.format_sql=true
|
||||
# \u663E\u793A\u7ED1\u5B9A\u53C2\u6570\u7684\u5177\u4F53\u503C\uFF08\u5173\u952E\u914D\u7F6E\uFF09
|
||||
logging.level.org.hibernate.type.descriptor.sql=TRACE
|
||||
# \u6587\u4EF6\u5927\u5C0F
|
||||
spring.servlet.multipart.max-file-size=2GB
|
||||
spring.servlet.multipart.max-request-size=2GB
|
||||
mybatis.type-aliases-package=com.mir4updater.backend.entity
|
||||
mybatis.mapperLocations=classpath:mapper/*.xml
|
||||
# redis\u57FA\u7840\u914D\u7F6E
|
||||
# 0\u4E5F\u662F\u9ED8\u8BA4\u503C\uFF0C\u8868\u793A\u4F60\u8981\u64CD\u63A7\u7684 Redis \u4E0A\u7684\u54EA\u4E2A\u6570\u636E\u5E93
|
||||
spring.data.redis.database=0
|
||||
# 6379\u4E5F\u662F\u9ED8\u8BA4\u503C\uFF0C\u8868\u793A Redis \u7AEF\u53E3
|
||||
spring.data.redis.port=6379
|
||||
# \u8FD9\u91CC\u586B\u5199\u4F60\u7684\u670D\u52A1\u5668\u5730\u5740
|
||||
spring.data.redis.host=127.0.0.1
|
||||
spring.data.redis.password=fanhuitong
|
||||
# \u53EF\u7701\u7565
|
||||
spring.data.redis.lettuce.pool.min-idle=5
|
||||
spring.data.redis.lettuce.pool.max-idle=10
|
||||
spring.data.redis.lettuce.pool.max-active=8
|
||||
spring.data.redis.lettuce.pool.max-wait=1ms
|
||||
spring.data.redis.lettuce.shutdown-timeout=100ms
|
||||
@@ -1,38 +1,3 @@
|
||||
spring.application.name=Mir4Updater
|
||||
server.port=65534
|
||||
## 数据连接信息
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.url=jdbc:mysql://127.0.0.1:3305/spring_boot?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
spring.datasource.username=tt
|
||||
spring.datasource.password=fanhuitong
|
||||
|
||||
# Hibernate配置
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
|
||||
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
|
||||
# 启用 SQL 日志
|
||||
spring.jpa.show-sql=true
|
||||
# 格式化 SQL
|
||||
#spring.jpa.properties.hibernate.format_sql=true
|
||||
# 显示绑定参数的具体值(关键配置)
|
||||
logging.level.org.hibernate.type.descriptor.sql=TRACE
|
||||
|
||||
# 文件大小
|
||||
spring.servlet.multipart.max-file-size=2GB
|
||||
spring.servlet.multipart.max-request-size=2GB
|
||||
|
||||
mybatis.type-aliases-package=com.mir4updater.backend.entity
|
||||
mybatis.mapperLocations=classpath:mapper/*.xml
|
||||
# redis基础配置
|
||||
# 0也是默认值,表示你要操控的 Redis 上的哪个数据库
|
||||
spring.data.redis.database=0
|
||||
# 6379也是默认值,表示 Redis 端口
|
||||
spring.data.redis.port=6379
|
||||
# 这里填写你的服务器地址
|
||||
spring.data.redis.host=127.0.0.1
|
||||
spring.data.redis.password=fanhuitong
|
||||
# 可省略
|
||||
spring.data.redis.lettuce.pool.min-idle=5
|
||||
spring.data.redis.lettuce.pool.max-idle=10
|
||||
spring.data.redis.lettuce.pool.max-active=8
|
||||
spring.data.redis.lettuce.pool.max-wait=1ms
|
||||
spring.data.redis.lettuce.shutdown-timeout=100ms
|
||||
# application.properties
|
||||
# \u9ED8\u8BA4\u6FC0\u6D3B\u751F\u4EA7\u73AF\u5883
|
||||
spring.profiles.active=prod
|
||||
|
||||
BIN
src/main/resources/static/favicon.ico
Normal file
BIN
src/main/resources/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Reference in New Issue
Block a user