usesPermissions) {
+ this.usesPermissions = usesPermissions;
+ }
+
+ public void addToUsesPermissions(String usesPermission) {
+ this.usesPermissions.add(usesPermission);
+ }
+
+ public String getVersionCode() {
+ return versionCode;
+ }
+
+ public void setVersionCode(String versionCode) {
+ this.versionCode = versionCode;
+ }
+
+ public String getVersionName() {
+ return versionName;
+ }
+
+ public void setVersionName(String versionName) {
+ this.versionName = versionName;
+ }
+
+ @Override
+ public String toString() {
+ return "ApkInfo [features=" + features + ", icon=" + icon + ", icons=" + icons + ", label=" + label + ", launchableActivity=" + launchableActivity + ", minSdkVersion=" + minSdkVersion + ", packageName=" + packageName + ", sdkVersion=" + sdkVersion + ", size=" + size + ", targetSdkVersion=" + targetSdkVersion + ", usesPermissions=" + usesPermissions + ", versionCode=" + versionCode + ", versionName=" + versionName + "]";
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/net/geedge/api/util/ApkUtil.java b/src/main/java/net/geedge/api/util/ApkUtil.java
new file mode 100644
index 0000000..572b058
--- /dev/null
+++ b/src/main/java/net/geedge/api/util/ApkUtil.java
@@ -0,0 +1,145 @@
+package net.geedge.api.util;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.log.Log;
+import net.geedge.common.Constant;
+import net.geedge.common.T;
+
+import java.io.*;
+import java.util.Base64;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class ApkUtil {
+
+ private static final Log log = Log.get();
+
+ public static final String APPLICATION = "application:";
+ public static final String APPLICATION_ICON = "application-icon";
+ public static final String APPLICATION_LABEL = "application-label";
+ public static final String APPLICATION_LABEL_N = "application: label";
+ public static final String DENSITIES = "densities";
+ public static final String LAUNCHABLE_ACTIVITY = "launchable";
+ public static final String PACKAGE = "package";
+ public static final String SDK_VERSION = "sdkVersion";
+ public static final String SUPPORTS_ANY_DENSITY = "support-any-density";
+ public static final String SUPPORTS_SCREENS = "support-screens";
+ public static final String TARGET_SDK_VERSION = "targetSdkVersion";
+ public static final String VERSION_CODE = "versionCode";
+ public static final String VERSION_NAME = "versionName";
+ public static final String USES_FEATURE = "uses-feature";
+ public static final String USES_IMPLIED_FEATURE = "uses-implied-feature";
+ public static final String USES_PERMISSION = "uses-permission";
+
+ private static final String SPLIT_REGEX = "(: )|(=')|(' )|'";
+
+ private ProcessBuilder builder;
+ // aapt 所在目录
+ private String aaptToolPath = "aapt";
+
+ public ApkUtil() {
+ builder = new ProcessBuilder();
+ builder.redirectErrorStream(true);
+ }
+
+ public String getAaptToolPath() {
+ return aaptToolPath;
+ }
+
+ public void setAaptToolPath(String aaptToolPath) {
+ this.aaptToolPath = aaptToolPath;
+ }
+
+ public ApkInfo parseApk(String apkPath) {
+ String aaptTool = aaptToolPath;
+ Process process = null;
+ InputStream inputStream = null;
+ BufferedReader bufferedReader = null;
+ try {
+ process = builder.command(aaptTool, "d", "badging", apkPath).start();
+ inputStream = process.getInputStream();
+ bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
+ ApkInfo apkInfo = new ApkInfo();
+ apkInfo.setSize(new File(apkPath).length());
+ String temp = null;
+ while ((temp = bufferedReader.readLine()) != null) {
+ setApkInfoProperty(apkInfo, temp);
+ }
+ return apkInfo;
+ } catch (IOException e) {
+ log.error(e, "[parseApk] [error]");
+ return null;
+ } finally {
+ if (process != null) {
+ process.destroy();
+ }
+ T.IoUtil.close(inputStream);
+ T.IoUtil.close(bufferedReader);
+ }
+ }
+
+ private void setApkInfoProperty(ApkInfo apkInfo, String source) {
+ if (source.startsWith(APPLICATION)) {
+ String[] rs = source.split("( icon=')|'");
+ apkInfo.setIcon(rs[rs.length - 1]);
+ } else if (source.startsWith(APPLICATION_ICON)) {
+ apkInfo.addToIcons(getKeyBeforeColon(source), getPropertyInQuote(source));
+ } else if (source.startsWith(APPLICATION_LABEL)) {
+ apkInfo.setLabel(getPropertyInQuote(source));
+ } else if (source.startsWith(LAUNCHABLE_ACTIVITY)) {
+ apkInfo.setLaunchableActivity(getPropertyInQuote(source));
+ } else if (source.startsWith(PACKAGE)) {
+ String[] packageInfo = source.split(SPLIT_REGEX);
+ apkInfo.setPackageName(packageInfo[2]);
+ apkInfo.setVersionCode(packageInfo[4]);
+ apkInfo.setVersionName(packageInfo[6]);
+ } else if (source.startsWith(SDK_VERSION)) {
+ apkInfo.setSdkVersion(getPropertyInQuote(source));
+ } else if (source.startsWith(TARGET_SDK_VERSION)) {
+ apkInfo.setTargetSdkVersion(getPropertyInQuote(source));
+ } else if (source.startsWith(USES_PERMISSION)) {
+ apkInfo.addToUsesPermissions(getPropertyInQuote(source));
+ } else if (source.startsWith(USES_FEATURE)) {
+ apkInfo.addToFeatures(getPropertyInQuote(source));
+ }
+ }
+
+ private String getKeyBeforeColon(String source) {
+ return source.substring(0, source.indexOf(':'));
+ }
+
+ private String getPropertyInQuote(String source) {
+ int index = source.indexOf("'") + 1;
+ return source.substring(index, source.indexOf('\'', index));
+ }
+
+ public String extractFileFromApk(String apkPath, String fileName) {
+ ZipFile zipFile = null;
+ File tempIconFile = T.FileUtil.file(Constant.TEMP_PATH, T.IdUtil.fastSimpleUUID());
+ try {
+ zipFile = new ZipFile(apkPath);
+ ZipEntry entry = zipFile.getEntry(fileName);
+ InputStream inputStream = zipFile.getInputStream(entry);
+ BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempIconFile), 1024);
+ byte[] b = new byte[1024];
+ BufferedInputStream bis = new BufferedInputStream(inputStream, 1024);
+ while (bis.read(b) != -1) {
+ bos.write(b);
+ }
+ IoUtil.flush(bos);
+ T.IoUtil.close(bos);
+ T.IoUtil.close(bis);
+ T.IoUtil.close(inputStream);
+ T.IoUtil.close(zipFile);
+
+ String base64Str = Base64.getEncoder().encodeToString(T.FileUtil.readBytes(tempIconFile));
+ return base64Str;
+ } catch (IOException e) {
+ log.error(e, "[extractFileFromApk] [error]");
+ } finally {
+ T.FileUtil.del(tempIconFile);
+ T.IoUtil.close(zipFile);
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/geedge/common/APIException.java b/src/main/java/net/geedge/common/APIException.java
new file mode 100644
index 0000000..d3ff76a
--- /dev/null
+++ b/src/main/java/net/geedge/common/APIException.java
@@ -0,0 +1,47 @@
+package net.geedge.common;
+
+import lombok.Data;
+
+/**
+ * 自定义异常
+ */
+@Data
+public class APIException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ private String msg;
+ private int code = RCode.ERROR.getCode();
+ private Object[] param = new Object[]{};
+ private RCode rCode;
+
+ public APIException(RCode rCode) {
+ super(rCode.getMsg());
+ this.code = rCode.getCode();
+ this.msg = rCode.getMsg();
+ this.param = rCode.getParam();
+ this.rCode = rCode;
+ }
+
+ public APIException(String msg) {
+ super(msg);
+ this.msg = msg;
+ }
+
+ public APIException(String msg, Throwable e) {
+ super(msg, e);
+ this.msg = msg;
+ }
+
+ public APIException(String msg, int code) {
+ super(msg);
+ this.msg = msg;
+ this.code = code;
+ }
+
+ public APIException(String msg, int code, Throwable e) {
+ super(msg, e);
+ this.msg = msg;
+ this.code = code;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/geedge/common/APIExceptionHandler.java b/src/main/java/net/geedge/common/APIExceptionHandler.java
new file mode 100644
index 0000000..89f25cc
--- /dev/null
+++ b/src/main/java/net/geedge/common/APIExceptionHandler.java
@@ -0,0 +1,39 @@
+package net.geedge.common;
+
+import cn.hutool.log.Log;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.catalina.connector.ClientAbortException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 异常处理器
+ */
+@RestControllerAdvice
+public class APIExceptionHandler {
+
+ private static final Log log = Log.get();
+
+ @ExceptionHandler(APIException.class)
+ @ResponseStatus(value = HttpStatus.BAD_REQUEST)
+ public R handleNZException(APIException e) {
+ R r = new R();
+ r.put("code", e.getCode());
+ r.put("msg", e.getMsg());
+ return r;
+ }
+
+ @ExceptionHandler(Exception.class)
+ @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
+ public R handleException(Exception e, HttpServletRequest request) {
+ if (e instanceof ClientAbortException) {
+ return null;
+ }
+ log.error(e, "Request uri: {}", request.getRequestURI());
+ return R.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/net/geedge/common/Constant.java b/src/main/java/net/geedge/common/Constant.java
new file mode 100644
index 0000000..b1b4f6b
--- /dev/null
+++ b/src/main/java/net/geedge/common/Constant.java
@@ -0,0 +1,17 @@
+package net.geedge.common;
+
+import java.io.File;
+
+public class Constant {
+ /**
+ * 临时目录
+ */
+ public static final String TEMP_PATH = System.getProperty("user.dir") + File.separator + "tmp";
+
+ static {
+ File tempPath = T.FileUtil.file(TEMP_PATH);
+ // 程序启动清空临时目录
+ T.FileUtil.del(tempPath);
+ T.FileUtil.mkdir(tempPath);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/geedge/common/R.java b/src/main/java/net/geedge/common/R.java
new file mode 100644
index 0000000..7468ab5
--- /dev/null
+++ b/src/main/java/net/geedge/common/R.java
@@ -0,0 +1,90 @@
+package net.geedge.common;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 返回数据
+ *
+ * 错误码、错误内容统一在枚举类RCode中定义, 错误码格式见RCode注释,错误码内容必须用英文,作为国际化的code 自定义的错误类型必须加注释
+ */
+public class R extends HashMap {
+
+ private static final long serialVersionUID = 1L;
+
+ public R() {
+ put("code", RCode.SUCCESS.getCode());
+ put("msg", RCode.SUCCESS.getMsg());
+ put("timestamp", T.DateUtil.current());
+ }
+
+ public static R ok() {
+ return new R();
+ }
+
+ public static R ok(String msg) {
+ R r = new R();
+ r.put("msg", msg);
+ return r;
+ }
+
+ public static R ok(Object data) {
+ R r = new R();
+ r.put("data", data);
+ return r;
+ }
+
+ public static R error() {
+ return error(RCode.ERROR.getCode(), RCode.ERROR.getMsg());
+ }
+
+ public static R error(RCode rCode) {
+ R r = new R();
+ r.put("code", rCode.getCode());
+ r.put("msg", rCode.getMsg());
+ return r;
+ }
+
+ public static R error(String msg) {
+ R r = new R();
+ r.put("code", RCode.ERROR.getCode());
+ r.put("msg", msg);
+ return r;
+ }
+
+ public static R error(Integer code, String msg) {
+ R r = new R();
+ r.put("code", code);
+ r.put("msg", msg);
+ return r;
+ }
+
+ @Override
+ public R put(String key, Object value) {
+ super.put(key, value);
+ return this;
+ }
+
+ public R putData(Object value) {
+ this.put("data", value);
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public R putData(String key, Object value) {
+ Object data = super.getOrDefault("data", new LinkedHashMap());
+ if (!(data instanceof Map)) {
+ throw new APIException("data put error");
+ }
+ ((Map) data).put(key, value);
+ super.put("data", data);
+ return this;
+ }
+
+ @SuppressWarnings("all")
+ public R putAllData(Map m) {
+ super.putAll(m);
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/geedge/common/RCode.java b/src/main/java/net/geedge/common/RCode.java
new file mode 100644
index 0000000..a7e6b97
--- /dev/null
+++ b/src/main/java/net/geedge/common/RCode.java
@@ -0,0 +1,42 @@
+package net.geedge.common;
+
+import java.text.MessageFormat;
+
+public enum RCode {
+
+
+ BAD_REQUEST(400, "Bad Request "),
+
+ NOT_EXISTS(404, "No such file or directory"),
+ NOT_PERMISSION(401 , "Permission denied"),
+
+ ERROR(999, "error"), // 通用错误/未知错误
+
+ SUCCESS(200, "success"); // 成功
+
+ RCode(Integer code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+ private Integer code;
+ private String msg;
+ private Object[] param;
+
+ public RCode setParam(Object... param) {
+ this.param = param;
+ return this;
+ }
+
+ public Object[] getParam() {
+ return param;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public String getMsg() {
+ return MessageFormat.format(msg, param);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/geedge/common/T.java b/src/main/java/net/geedge/common/T.java
new file mode 100644
index 0000000..2545cfb
--- /dev/null
+++ b/src/main/java/net/geedge/common/T.java
@@ -0,0 +1,215 @@
+package net.geedge.common;
+
+import cn.hutool.core.io.IORuntimeException;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.MediaType;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+
+public class T {
+ /**
+ * 时间工具类
+ */
+ public static class DateUtil extends cn.hutool.core.date.DateUtil {
+ }
+
+ /**
+ * 字符串工具类
+ */
+ public static class StrUtil extends cn.hutool.core.util.StrUtil {
+ }
+ /**
+ * 反射工具类
+ *
+ * @author Looly
+ * @since 3.0.9
+ */
+ public static class ReflectUtil extends cn.hutool.core.util.ReflectUtil {
+ }
+
+ /**
+ * Map相关工具类
+ */
+ public static class MapUtil extends cn.hutool.core.map.MapUtil {
+ }
+
+ /**
+ * 集合工具类
+ */
+ public static class ListUtil extends cn.hutool.core.collection.ListUtil {
+ }
+ /**
+ * 字符集工具类
+ *
+ * @author xiaoleilu
+ */
+ public static class CharsetUtil extends cn.hutool.core.util.CharsetUtil {
+ }
+ /**
+ * ID生成器工具类,此工具类中主要封装:
+ *
+ *
+ * 1. 唯一性ID生成器:UUID、ObjectId(MongoDB)、Snowflake
+ *
+ *
+ *
+ * ID相关文章见:http://calvin1978.blogcn.com/articles/uuid.html
+ *
+ * @author looly
+ * @since 4.1.13
+ */
+ public static class IdUtil extends cn.hutool.core.util.IdUtil {
+ }
+ /**
+ * 线程池工具
+ *
+ * @author luxiaolei
+ */
+ public static class ThreadUtil extends cn.hutool.core.thread.ThreadUtil {
+ }
+ /**
+ * json 工具类
+ */
+ public static class JSONUtil extends cn.hutool.json.JSONUtil {
+ }
+ /**
+ * 系统运行时工具类,用于执行系统命令的工具
+ *
+ * @author Looly
+ * @since 3.1.1
+ */
+ public static class RuntimeUtil extends cn.hutool.core.util.RuntimeUtil {
+ }
+
+ /**
+ * 文件工具类
+ *
+ * @author looly
+ */
+ public static class FileUtil extends cn.hutool.core.io.FileUtil {
+ }
+ /**
+ * 压缩工具类
+ *
+ * @author Looly
+ */
+ public static class ZipUtil extends cn.hutool.core.util.ZipUtil {
+ }
+ /**
+ * IO工具类
+ * IO工具类只是辅助流的读写,并不负责关闭流。原因是流可能被多次读写,读写关闭后容易造成问题。
+ *
+ * @author xiaoleilu
+ */
+ public static class IoUtil extends cn.hutool.core.io.IoUtil {
+ }
+ /**
+ * URL(Uniform Resource Locator)统一资源定位符相关工具类
+ *
+ *
+ * 统一资源定位符,描述了一台特定服务器上某资源的特定位置。
+ *
+ * URL组成:
+ *
+ *
+ * 协议://主机名[:端口]/ 路径/[:参数] [?查询]#Fragment
+ * protocol :// hostname[:port] / path / [:parameters][?query]#fragment
+ *
+ *
+ * @author xiaoleilu
+ */
+ public static class URLUtil extends cn.hutool.core.util.URLUtil {
+ }
+
+ /**
+ * CommandLineUtil
+ *
+ * @version org.apache.commons.commons-exec:1.3
+ * @apiNote copy from rg.apache.commons.exec.CommandLine.translateCommandline
+ */
+ public static class CommandLineUtil {
+ /**
+ * translateCommandline
+ *
+ * @param toProcess
+ * @return
+ */
+ public static String[] translateCommandline(String toProcess) {
+ if (toProcess != null && toProcess.length() != 0) {
+ int state = 0;
+ StringTokenizer tok = new StringTokenizer(toProcess, "\"' ", true);
+ ArrayList list = new ArrayList();
+ StringBuilder current = new StringBuilder();
+ boolean lastTokenHasBeenQuoted = false;
+
+ while (true) {
+ while (tok.hasMoreTokens()) {
+ String nextTok = tok.nextToken();
+ switch (state) {
+ case 1:
+ if ("'".equals(nextTok)) {
+ lastTokenHasBeenQuoted = true;
+ state = 0;
+ } else {
+ current.append(nextTok);
+ }
+ continue;
+ case 2:
+ if ("\"".equals(nextTok)) {
+ lastTokenHasBeenQuoted = true;
+ state = 0;
+ } else {
+ current.append(nextTok);
+ }
+ continue;
+ }
+
+ if ("'".equals(nextTok)) {
+ state = 1;
+ } else if ("\"".equals(nextTok)) {
+ state = 2;
+ } else if (" ".equals(nextTok)) {
+ if (lastTokenHasBeenQuoted || current.length() != 0) {
+ list.add(current.toString());
+ current = new StringBuilder();
+ }
+ } else {
+ current.append(nextTok);
+ }
+
+ lastTokenHasBeenQuoted = false;
+ }
+
+ if (lastTokenHasBeenQuoted || current.length() != 0) {
+ list.add(current.toString());
+ }
+
+ if (state != 1 && state != 2) {
+ String[] args = new String[list.size()];
+ return (String[]) list.toArray(args);
+ }
+
+ throw new IllegalArgumentException("Unbalanced quotes in " + toProcess);
+ }
+ } else {
+ return new String[0];
+ }
+ }
+ }
+
+ public static class ResponseUtil {
+ /**
+ * reponse 下载 byte数据
+ */
+ public static void downloadFile(HttpServletResponse response, String filename, byte[] data) throws IORuntimeException, IOException {
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ String fileName = T.URLUtil.encode(filename, T.CharsetUtil.CHARSET_UTF_8);
+ cn.hutool.core.util.ReflectUtil.invoke(response, "addHeader", "Content-Disposition", "attachment; filename=" + fileName);
+ cn.hutool.core.util.ReflectUtil.invoke(response, "addHeader", "Content-Length", "" + data.length);
+ cn.hutool.core.util.ReflectUtil.invoke(response, "setHeader", "Access-Control-Expose-Headers", "Content-Disposition");
+ T.IoUtil.write(response.getOutputStream(), false, data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..e86d68f
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,12 @@
+spring:
+ profiles:
+ active: prod
+ servlet:
+ context-path: /
+ multipart:
+ max-file-size: 500MB
+ max-request-size: 500MB
+ enabled: true
+
+logging:
+ config:./config/logback-spring.xml
diff --git a/src/test/java/net/geedge/TestJ.java b/src/test/java/net/geedge/TestJ.java
new file mode 100644
index 0000000..3d2f79f
--- /dev/null
+++ b/src/test/java/net/geedge/TestJ.java
@@ -0,0 +1,8 @@
+package net.geedge;
+
+public class TestJ {
+
+ public static void main(String[] args) {
+
+ }
+}
\ No newline at end of file