diff options
| author | shizhendong <[email protected]> | 2024-08-22 09:22:52 +0800 |
|---|---|---|
| committer | shizhendong <[email protected]> | 2024-08-22 09:22:52 +0800 |
| commit | 3e306e1a8c30655be7147751c51399a646cdef04 (patch) | |
| tree | 0f2a00e36fcba23d02b20b729a813e5fbc3ff290 /src | |
| parent | 94a42089e7c477e4783094f21437178b23cc9e97 (diff) | |
init: ASW-37 初始化 device-api
Diffstat (limited to 'src')
18 files changed, 1888 insertions, 0 deletions
diff --git a/src/main/java/net/geedge/DeviceApiApplication.java b/src/main/java/net/geedge/DeviceApiApplication.java new file mode 100644 index 0000000..8cb0821 --- /dev/null +++ b/src/main/java/net/geedge/DeviceApiApplication.java @@ -0,0 +1,48 @@ +package net.geedge; + +import cn.hutool.extra.spring.EnableSpringUtil; +import net.geedge.api.entity.DeviceApiYml; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +import java.util.TimeZone; + +@EnableSpringUtil +@SpringBootApplication +public class DeviceApiApplication { + + public static void main(String[] args) { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + SpringApplication.run(DeviceApiApplication.class, args); + } + + @Autowired + private Environment env; + + @Bean + public DeviceApiYml deviceProperties() { + DeviceApiYml apiYml = new DeviceApiYml(); + + DeviceApiYml.Device device = apiYml.new Device(); + device.setRoot(env.getProperty("device.root")); + device.setType(env.getProperty("device.type")); + device.setPlatform(env.getProperty("device.platform")); + + DeviceApiYml.Adb adb = apiYml.new Adb(); + adb.setSerial(env.getProperty("adb.serial")); + adb.setHost(env.getProperty("adb.host")); + adb.setPort(env.getProperty("adb.port", Integer.class)); + + DeviceApiYml.Vnc vnc = apiYml.new Vnc(); + vnc.setHost(env.getProperty("vnc.host")); + vnc.setPort(env.getProperty("vnc.port", Integer.class)); + + apiYml.setDevice(device); + apiYml.setAdb(adb); + apiYml.setVnc(vnc); + return apiYml; + } +}
\ No newline at end of file diff --git a/src/main/java/net/geedge/api/config/VncProxyHandler.java b/src/main/java/net/geedge/api/config/VncProxyHandler.java new file mode 100644 index 0000000..a226568 --- /dev/null +++ b/src/main/java/net/geedge/api/config/VncProxyHandler.java @@ -0,0 +1,84 @@ +package net.geedge.api.config; + +import cn.hutool.log.Log; +import net.geedge.api.entity.DeviceApiYml; +import net.geedge.common.T; +import org.springframework.web.socket.BinaryMessage; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; + +public class VncProxyHandler extends TextWebSocketHandler { + + private static final Log log = Log.get(); + + private DeviceApiYml.Vnc vnc; + + public VncProxyHandler(DeviceApiYml.Vnc vnc) { + this.vnc = vnc; + } + + @Override + public synchronized void afterConnectionEstablished(WebSocketSession session) throws Exception { + log.info("[afterConnectionEstablished] [WebSocket connection established] [websocket uri: {}]", session.getUri()); + super.afterConnectionEstablished(session); + + // connect to VNC Server + Socket vncSocket = new Socket(vnc.getHost(), vnc.getPort()); + session.getAttributes().put("vncSocket", vncSocket); + log.info("[afterConnectionEstablished] [vnc server: {}] [isConnected: {}]", T.JSONUtil.toJsonStr(vnc), vncSocket.isConnected()); + + // vnc server -> web + T.ThreadUtil.execute(() -> { + this.forwardFromVncToWeb(session, vncSocket); + }); + } + + /** + * vnc server -> web + * + * @param session + * @param vncSocket + */ + private void forwardFromVncToWeb(WebSocketSession session, Socket vncSocket) { + try (InputStream inputStream = vncSocket.getInputStream()) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + session.sendMessage(new BinaryMessage(buffer, 0, bytesRead, true)); + } + } catch (IOException e) { + log.error(e, "[forwardFromVncToWeb] [error]"); + } + } + + @Override + protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) { + try { + // web -> vnc server + Socket vncSocket = (Socket) session.getAttributes().get("vncSocket"); + if (vncSocket != null && !vncSocket.isClosed()) { + OutputStream outputStream = vncSocket.getOutputStream(); + outputStream.write(message.getPayload().array()); + outputStream.flush(); + } + } catch (IOException e) { + log.error(e, "[handleBinaryMessage] [error]"); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + log.info("[afterConnectionClosed] [WebSocket connection closed] [websocket uri: {}]", session.getUri()); + Socket vncSocket = (Socket) session.getAttributes().get("vncSocket"); + if (vncSocket != null && !vncSocket.isClosed()) { + vncSocket.close(); + } + super.afterConnectionClosed(session, status); + } +}
\ No newline at end of file diff --git a/src/main/java/net/geedge/api/config/WebSocketConfig.java b/src/main/java/net/geedge/api/config/WebSocketConfig.java new file mode 100644 index 0000000..453d972 --- /dev/null +++ b/src/main/java/net/geedge/api/config/WebSocketConfig.java @@ -0,0 +1,21 @@ +package net.geedge.api.config; + +import net.geedge.api.entity.DeviceApiYml; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Autowired + private DeviceApiYml deviceApiYml; + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(new VncProxyHandler(deviceApiYml.getVnc()), "/api/v1/device/websocket").setAllowedOrigins("*"); + } +} diff --git a/src/main/java/net/geedge/api/controller/APIController.java b/src/main/java/net/geedge/api/controller/APIController.java new file mode 100644 index 0000000..58e8c73 --- /dev/null +++ b/src/main/java/net/geedge/api/controller/APIController.java @@ -0,0 +1,166 @@ +package net.geedge.api.controller; + +import cn.hutool.core.codec.Base32Codec; +import jakarta.servlet.http.HttpServletResponse; +import net.geedge.api.entity.DeviceApiYml; +import net.geedge.api.util.AdbUtil; +import net.geedge.common.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/device") +public class APIController { + + private final AdbUtil adbUtil; + + @Autowired + public APIController(DeviceApiYml deviceApiYml) { + this.adbUtil = AdbUtil.getInstance(deviceApiYml.getAdb()); + } + + @GetMapping("/status") + public R status() { + return R.ok(adbUtil.status()); + } + + @PostMapping("/file") + public R push(@RequestParam(value = "file") MultipartFile file, @RequestParam String path) throws IOException { + File tempFile = null; + try { + tempFile = T.FileUtil.file(Constant.TEMP_PATH, file.getOriginalFilename()); + file.transferTo(tempFile); + + AdbUtil.CommandResult result = adbUtil.push(tempFile.getAbsolutePath(), path); + if (0 != result.exitCode()) { + return R.error(result.output()); + } + } finally { + T.FileUtil.del(tempFile); + } + return R.ok(); + } + + @GetMapping("/file/{fileId}") + public void pull(@PathVariable String fileId, HttpServletResponse response) throws IOException { + byte[] decode = Base32Codec.Base32Decoder.DECODER.decode(fileId); + String filePath = T.StrUtil.str(decode, T.CharsetUtil.CHARSET_UTF_8); + String fileName = T.FileUtil.getName(filePath); + + File tempFile = T.FileUtil.file(Constant.TEMP_PATH, fileName); + try { + AdbUtil.CommandResult result = adbUtil.pull(filePath, tempFile.getAbsolutePath()); + if (0 != result.exitCode()) { + throw new APIException(result.output()); + } + if (T.FileUtil.isDirectory(tempFile)) { + File zip = T.ZipUtil.zip(tempFile); + try { + T.ResponseUtil.downloadFile(response, zip.getName(), T.FileUtil.readBytes(zip)); + } finally { + T.FileUtil.del(zip); + } + } else { + T.ResponseUtil.downloadFile(response, fileName, T.FileUtil.readBytes(tempFile)); + } + } finally { + T.FileUtil.del(tempFile); + } + } + + @GetMapping("/file") + public R listDir(@RequestParam(defaultValue = "/") String path) { + List<Map> listDir = adbUtil.listDir(path); + Map<Object, Object> data = T.MapUtil.builder() + .put("path", path) + .put("records", listDir) + .build(); + return R.ok(data); + } + + @GetMapping("/app") + public R listApp(@RequestParam(required = false) String arg) { + return R.ok().putData("records", adbUtil.listApp(arg)); + } + + @PostMapping("/app") + public R install(@RequestParam(value = "file", required = false) MultipartFile file, + @RequestParam(required = false) String path) throws IOException { + if (file != null) { + File tempFile = null; + try { + tempFile = T.FileUtil.file(Constant.TEMP_PATH, file.getOriginalFilename()); + file.transferTo(tempFile); + + AdbUtil.CommandResult result = adbUtil.install(tempFile.getAbsolutePath(), true, true); + if (0 != result.exitCode()) { + throw new APIException(result.output()); + } + return R.ok(); + } finally { + T.FileUtil.del(tempFile); + } + } + + if (T.StrUtil.isNotEmpty(path)) { + AdbUtil.CommandResult result = adbUtil.install(path, true, true); + if (0 != result.exitCode()) { + throw new APIException(result.output()); + } + return R.ok(); + } + return R.error(RCode.BAD_REQUEST); + } + + @DeleteMapping("/app") + public R uninstall(@RequestParam String packageName) { + AdbUtil.CommandResult result = adbUtil.uninstall(packageName); + if (0 != result.exitCode()) { + throw new APIException(result.output()); + } + return R.ok(); + } + + @PostMapping("/pcap") + public R startTcpdump(@RequestParam(required = false, defaultValue = "") String packageName) { + AdbUtil.CommandResult result = adbUtil.startTcpdump(packageName); + if (0 != result.exitCode()) { + throw new APIException("exec tcpdump error"); + } + return R.ok().putData("id", result.output()); + } + + @DeleteMapping("/pcap") + public void stopTcpdump(@RequestParam String id, + @RequestParam(required = false, defaultValue = "false") Boolean returnFile, + HttpServletResponse response) throws IOException { + AdbUtil.CommandResult result = adbUtil.stopTcpdump(id); + if (0 != result.exitCode()) { + throw new APIException(result.output()); + } + + if (returnFile) { + // response pcap file + File tempFile = T.FileUtil.file(Constant.TEMP_PATH, id + ".pcap"); + try { + String filePath = "/data/local/tmp/" + id + ".pcap"; + AdbUtil.CommandResult pulled = adbUtil.pull(filePath, tempFile.getAbsolutePath()); + if (0 != pulled.exitCode()) { + throw new APIException(pulled.output()); + } + T.ResponseUtil.downloadFile(response, tempFile.getName(), T.FileUtil.readBytes(tempFile)); + } finally { + T.FileUtil.del(tempFile); + } + } else { + // response taskid + response.getWriter().write(T.JSONUtil.toJsonStr(R.ok().putData("id", id))); + } + } +}
\ No newline at end of file diff --git a/src/main/java/net/geedge/api/entity/DeviceApiYml.java b/src/main/java/net/geedge/api/entity/DeviceApiYml.java new file mode 100644 index 0000000..d939028 --- /dev/null +++ b/src/main/java/net/geedge/api/entity/DeviceApiYml.java @@ -0,0 +1,32 @@ +package net.geedge.api.entity; + +import lombok.Data; + +@Data +public class DeviceApiYml { + + private Device device; + private Adb adb; + private Vnc vnc; + + @Data + public class Device { + String type; + String platform; + String root; + } + + @Data + public class Adb { + String serial; + String host; + Integer port; + } + + @Data + public class Vnc { + String host; + Integer port; + } + +}
\ No newline at end of file diff --git a/src/main/java/net/geedge/api/util/AdbCommandBuilder.java b/src/main/java/net/geedge/api/util/AdbCommandBuilder.java new file mode 100644 index 0000000..5335d41 --- /dev/null +++ b/src/main/java/net/geedge/api/util/AdbCommandBuilder.java @@ -0,0 +1,144 @@ +package net.geedge.api.util; + +import net.geedge.common.T; + +import java.util.LinkedList; +import java.util.List; + +public class AdbCommandBuilder { + + private final String adbPath; + private final List<String> command; + + private AdbCommandBuilder(String adbPath) { + this.adbPath = adbPath; + this.command = new LinkedList<>(); + this.command.add(adbPath); + } + + public static AdbCommandBuilder builder() { + return new AdbCommandBuilder("adb"); + } + + public static AdbCommandBuilder builder(String adbPath) { + return new AdbCommandBuilder(adbPath); + } + + public AdbCommandBuilder serial(String serial) { + this.command.add("-s"); + this.command.add(serial); + return this; + } + + public AdbCommandBuilder buildConnectCommand(String host, Integer port) { + this.command.add("connect"); + this.command.add(String.format("%s:%s", host, port)); + return this; + } + + public AdbCommandBuilder buildDevicesCommand() { + this.command.add("devices"); + this.command.add("-l"); + return this; + } + + public AdbCommandBuilder buildRootCommand() { + this.command.add("root"); + return this; + } + + public AdbCommandBuilder buildGetpropCommand() { + this.command.add("shell"); + this.command.add("getprop"); + return this; + } + + public AdbCommandBuilder buildWmSizeCommand() { + this.command.add("shell"); + this.command.add("wm"); + this.command.add("size"); + return this; + } + + public AdbCommandBuilder buildCheckRootCommand() { + this.command.add("shell"); + this.command.add("ls"); + this.command.add("/data"); + return this; + } + + /** + * 指定 String cmd 执行,解决命令过长阅读性较差问题 + */ + public AdbCommandBuilder buildShellCommand(String shellCmd) { + String[] strings = T.CommandLineUtil.translateCommandline(shellCmd); + for (String string : strings) { + this.command.add(string); + } + return this; + } + + public AdbCommandBuilder buildPushCommand(String local, String remote) { + this.command.add("push"); + this.command.add(local); + this.command.add(remote); + return this; + } + + public AdbCommandBuilder buildPullCommand(String remote, String local) { + this.command.add("pull"); + this.command.add(remote); + this.command.add(local); + return this; + } + + public AdbCommandBuilder buildLsDirCommand(String path) { + this.command.add("shell"); + this.command.add("ls"); + this.command.add("-l"); + this.command.add(path); + return this; + } + + public AdbCommandBuilder buildPmListPackagesCommand(String arg) { + this.command.add("shell"); + this.command.add("pm"); + this.command.add("list"); + this.command.add("packages"); + if (T.StrUtil.isNotEmpty(arg)) { + this.command.add(arg); + } + return this; + } + + public AdbCommandBuilder buildMd5sumCommand(String path) { + this.command.add("shell"); + this.command.add("md5sum"); + this.command.add(path); + return this; + } + + public AdbCommandBuilder buildInstallCommand(String localFilePath, boolean isDebugApk, boolean isReInstall) { + this.command.add("install"); + if (isDebugApk) { + this.command.add("-d"); + } + if (isReInstall) { + this.command.add("-r"); + } + this.command.add(localFilePath); + return this; + } + + public AdbCommandBuilder buildUnInstallCommand(String packageName) { + this.command.add("uninstall"); + this.command.add(packageName); + return this; + } + + + public List<String> build() { + return this.command; + } + +}
\ No newline at end of file diff --git a/src/main/java/net/geedge/api/util/AdbDevice.java b/src/main/java/net/geedge/api/util/AdbDevice.java new file mode 100644 index 0000000..5dd1d01 --- /dev/null +++ b/src/main/java/net/geedge/api/util/AdbDevice.java @@ -0,0 +1,41 @@ +package net.geedge.api.util; + +import lombok.Data; +import net.geedge.common.T; + +@Data +public class AdbDevice implements Comparable<AdbDevice> { + + private String serial; + private boolean available; + + + public AdbDevice(String line) { + String[] array = line.split(" "); + serial = array[0]; + + for (int i = 1; i < array.length; i++) { + if (!T.StrUtil.isEmpty(array[i])) { + available = "device".equals(array[i]); + break; + } + } + } + + public boolean isAvailable() { + return available; + } + + @Override + public boolean equals(Object object) { + if (object instanceof AdbDevice) + return serial.equals(((AdbDevice) object).serial) && available == ((AdbDevice) object).available; + return false; + } + + @Override + public int compareTo(AdbDevice device) { + return serial.compareTo(device.serial); + } + +}
\ No newline at end of file diff --git a/src/main/java/net/geedge/api/util/AdbUtil.java b/src/main/java/net/geedge/api/util/AdbUtil.java new file mode 100644 index 0000000..ed901b2 --- /dev/null +++ b/src/main/java/net/geedge/api/util/AdbUtil.java @@ -0,0 +1,568 @@ +package net.geedge.api.util; + +import cn.hutool.core.codec.Base32Codec; +import cn.hutool.core.thread.NamedThreadFactory; +import cn.hutool.log.Log; +import net.geedge.api.entity.DeviceApiYml; +import net.geedge.common.APIException; +import net.geedge.common.Constant; +import net.geedge.common.RCode; +import net.geedge.common.T; + +import java.io.File; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AdbUtil { + + private static final Log log = Log.get(); + + private static AdbUtil instance; + + private String serial; + private String host; + private Integer port; + + private ExecutorService threadPool; + + public String getSerial() { + return T.StrUtil.isNotEmpty(this.serial) ? serial : String.format("%s:%s", this.host, this.port); + } + + public record CommandResult(Integer exitCode, String output) { + } + + private AdbUtil(DeviceApiYml.Adb adb) { + this.serial = T.StrUtil.emptyToDefault(adb.getSerial(), ""); + this.host = adb.getHost(); + this.port = adb.getPort(); + this.connect(); + } + + public static AdbUtil getInstance(DeviceApiYml.Adb connInfo) { + if (instance == null) { + synchronized (AdbUtil.class) { + if (instance == null) { + instance = new AdbUtil(connInfo); + } + } + } + return instance; + } + + /** + * connect + */ + private void connect() { + if (T.StrUtil.isNotEmpty(this.serial)) { + // local + AdbDevice adbDevice = this.getAdbDevice(); + log.info("[connect] [result: {}]", T.JSONUtil.toJsonStr(adbDevice)); + if (null == adbDevice || !adbDevice.isAvailable()) { + log.error("[device is not available, program exit]"); + Runtime.getRuntime().halt(1); + } + } else { + // remote + String result = CommandExec.exec(AdbCommandBuilder.builder() + .buildConnectCommand(this.host, this.port) + .build()); + log.info("[connect] [result: {}]", result); + if (!T.StrUtil.contains(result, "connected")) { + log.error("[connect error, program exit]"); + Runtime.getRuntime().halt(1); + } + } + // adb root + CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildRootCommand() + .build() + ); + } + + /** + * status + * + * @return + */ + public Map<Object, Object> status() { + Map<Object, Object> m = T.MapUtil.builder() + .put("platform", "android") + .build(); + AdbDevice device = this.getAdbDevice(); + m.put("status", device.isAvailable() ? "online" : "offline"); + + Map<String, String> prop = this.getProp(); + m.put("name", T.MapUtil.getStr(prop, "ro.product.name", "")); + m.put("brand", T.MapUtil.getStr(prop, "ro.product.brand", "")); + m.put("model", T.MapUtil.getStr(prop, "ro.product.model", "")); + m.put("version", T.MapUtil.getStr(prop, "ro.build.version.release", "")); + m.put("resolution", T.MapUtil.getStr(prop, "wm.size", "")); + + // 默认为真机 + String type = "device"; + for (Map.Entry<String, String> entry : prop.entrySet()) { + // 根据 ro.build.* 这一组配置值判定是否为模拟器,如果包含 emulator、sdk 则为模拟器 + if (entry.getKey().contains("ro.build")) { + String value = entry.getValue(); + if (T.StrUtil.containsAnyIgnoreCase(value, "emulator", "sdk")) { + type = "emulator"; + break; + } + } + } + m.put("type", type); + + // check root + String checkRootResult = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildCheckRootCommand() + .build() + ); + m.put("root", !T.StrUtil.containsIgnoreCase(checkRootResult, "Permission denied")); + return m; + } + + /** + * getAdbDevice + * adb devices -l + * + * @return + */ + private AdbDevice getAdbDevice() { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .buildDevicesCommand() + .build() + ); + List<AdbDevice> list = T.ListUtil.list(true); + String[] lines = result.split("\\n"); + for (String line : lines) { + if (line.startsWith("*") || line.startsWith("List") || T.StrUtil.isEmpty(line)) + continue; + list.add(new AdbDevice(line)); + } + AdbDevice adbDevice = list.stream() + .filter(pojo -> T.StrUtil.equals(pojo.getSerial(), this.getSerial())) + .findFirst() + .orElse(null); + return adbDevice; + } + + /** + * getProp + * adb shell getprop + * + * @return + */ + private Map<String, String> getProp() { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildGetpropCommand() + .build() + ); + Map<String, String> prop = new LinkedHashMap<>(); + + Pattern pattern = Pattern.compile("\\[(.*?)\\]: \\[(.*?)\\]"); + Matcher matcher = pattern.matcher(result); + + while (matcher.find()) { + String key = matcher.group(1).trim(); + String value = matcher.group(2).trim(); + prop.put(key, value); + } + + // 分辨率 Physical size: 1440x3040 + String wmSize = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildWmSizeCommand() + .build() + ); + prop.put("wm.size", T.StrUtil.trim(wmSize.replaceAll("Physical size: ", ""))); + return prop; + } + + + /** + * md5sum + */ + private CommandResult md5sum(String path) { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildMd5sumCommand(path) + .build() + ); + log.info("[md5sum] [path: {}] [result: {}]", path, result); + if (T.StrUtil.isNotEmpty(result)) { + String md5 = result.split("\\s+")[0]; + return new CommandResult(0, md5); + } + return new CommandResult(1, ""); + } + + /** + * push + * 0 success; !0 failed + */ + public CommandResult push(String local, String remote) { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildPushCommand(local, remote) + .build() + ); + log.info("[push] [local: {}] [remote: {}] [result: {}]", local, remote, result); + return new CommandResult(T.StrUtil.contains(result, "failed") ? 1 : 0, result); + } + + /** + * pull + * 0 success; !0 failed + */ + public CommandResult pull(String remote, String local) { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildPullCommand(remote, local) + .build() + ); + log.info("[pull] [remote: {}] [local: {}] [result: {}]", remote, local, result); + return new CommandResult(T.StrUtil.containsAny(result, "file pulled", "files pulled") ? 0 : 1, result); + } + + /** + * list dir + * ls -l + * stat filename + */ + public List<Map> listDir(String path) { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildLsDirCommand(path) + .build() + ); + if (T.StrUtil.contains(result, "No such file or directory")) { + log.warn("[listDir] [path: {}] [result: {}]", path, result); + throw new APIException(RCode.NOT_EXISTS); + } + + if (T.StrUtil.contains(result, "Permission denied")) { + log.warn("[listDir] [path: {}] [result: {}]", path, result); + throw new APIException(RCode.NOT_PERMISSION); + } + + List<CompletableFuture<Map>> futureList = T.ListUtil.list(false); + + List<Map> listDir = T.ListUtil.list(true); + String[] lines = result.split("\\n"); + boolean isDir = false; + for (String line : lines) { + if (line.startsWith("total")) { + isDir = true; + continue; + } + String[] split = line.split("\\s+"); + String name; + // link file|dir + if (10 == split.length) { + name = split[7]; + } else { + name = split[split.length - 1]; + } + + String statFilePath = isDir ? Paths.get(path).resolve(name).toString() : path; + String statCommand = "shell stat -c \"'%N %a %X %Y'\" " + statFilePath; + futureList.add( + CompletableFuture.supplyAsync(() -> { + String statResult = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(statCommand.replaceAll("\\\\", "/")) + .build() + ); + // reverse result + List<String> list = Arrays.asList(statResult.split("\\s+")); + Collections.reverse(list); + + String fullName = list.get(3).replaceAll("'|`", ""); + Map<String, Object> relMap = T.MapUtil.newHashMap(); + relMap.put("name", name); + relMap.put("value", T.MapUtil.builder() + .put("id", Base32Codec.Base32Encoder.ENCODER.encode(fullName.getBytes())) + .put("fullName", fullName) + .put("permissions", Long.parseLong(list.get(2))) + .put("cts", Long.parseLong(list.get(1))) + .put("uts", Long.parseLong(list.get(0))) + .build()); + return relMap; + }, getThreadPool()) + ); + + Map<Object, Object> m = T.MapUtil.builder() + .put("name", name) + .build(); + listDir.add(m); + } + + try { + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get(); + futureList.forEach(f -> { + Map map = f.getNow(null); + if (T.MapUtil.isNotEmpty(map)) { + String name = T.MapUtil.getStr(map, "name"); + Map fileAttr = listDir.stream().filter(m -> T.MapUtil.getStr(m, "name").equals(name)).findFirst().get(); + fileAttr.putAll(T.MapUtil.get(map, "value", Map.class)); + } + }); + } catch (Exception e) { + log.warn(e); + } + return listDir; + } + + /** + * listApp + * adb shell pm list packages + * + * @return + */ + public List<Map> listApp(String arg) { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildPmListPackagesCommand(arg) + .build() + ); + + List<Map> listApp = T.ListUtil.list(true); + + List<CompletableFuture<Map>> futureList = T.ListUtil.list(false); + + String prefix = "package:"; + String[] lines = result.split("\\n"); + for (String line : lines) { + String packageName = T.StrUtil.trim(line.substring(prefix.length())); + + String dumpsysResult = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand("shell dumpsys package " + packageName) + .build() + ); + String[] split = dumpsysResult.split("\\n"); + String version = "", apkPath = ""; + for (String s : split) { + if (s.contains("versionName=")) { + version = T.StrUtil.trim(s).replaceAll("versionName=", ""); + } + if (s.contains("path: ")) { + apkPath = T.StrUtil.trim(s).replaceAll("path: ", ""); + } + } + if (T.StrUtil.isNotEmpty(apkPath)) { + String finalApkPath = apkPath; + futureList.add( + CompletableFuture.supplyAsync(() -> { + try { + CommandResult md5sumRes = this.md5sum(finalApkPath); + String md5Value = md5sumRes.output(); + File localApk = T.FileUtil.file(Constant.TEMP_PATH, md5Value + ".apk"); + if (!T.FileUtil.exist(localApk)) { + CommandResult pulled = this.pull(finalApkPath, localApk.getAbsolutePath()); + if (0 != pulled.exitCode()) { + log.warn("[listApp] [pull apk error] [pkg: {}]", packageName); + return null; + } + } + ApkUtil apkUtil = new ApkUtil(); + ApkInfo apkInfo = apkUtil.parseApk(localApk.getAbsolutePath()); + String appName = apkInfo.getLabel(); + String iconFilename = apkInfo.getIcon(); + String base64IconDate = apkUtil.extractFileFromApk(localApk.getAbsolutePath(), iconFilename); + + Map<String, Object> relMap = T.MapUtil.newHashMap(); + relMap.put("pkg", packageName); + relMap.put("value", T.MapUtil.builder() + .put("name", appName) + .put("icon", base64IconDate) + .build()); + return relMap; + } catch (Exception e) { + log.error(e, "[listApp] [parse apk] [pkg: {}]", packageName); + } + return null; + }, getThreadPool()) + ); + } + + Map<Object, Object> m = T.MapUtil.builder() + .put("packageName", packageName) + .put("version", version) + .build(); + listApp.add(m); + } + + try { + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get(); + futureList.forEach(f -> { + Map map = f.getNow(null); + if (T.MapUtil.isNotEmpty(map)) { + String pkg = T.MapUtil.getStr(map, "pkg"); + Map appAttr = listApp.stream().filter(m -> T.MapUtil.getStr(m, "packageName").equals(pkg)).findFirst().get(); + appAttr.putAll(T.MapUtil.get(map, "value", Map.class)); + } + }); + } catch (Exception e) { + log.warn(e); + } + return listApp; + } + + /** + * install app + * adb install apk + */ + public CommandResult install(String localFilePath, boolean isDebugApk, boolean isReInstall) { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildInstallCommand(localFilePath, isDebugApk, isReInstall) + .build() + ); + log.info("[install] [localFilePath: {}] [isDebugApk: {}] [isReInstall: {}] [result: {}]", localFilePath, isDebugApk, isReInstall, result); + return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result); + } + + /** + * uninstall app + * adb uninstall package_name + */ + public CommandResult uninstall(String packageName) { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildUnInstallCommand(packageName) + .build() + ); + log.info("[uninstall] [packageName: {}] [result: {}]", packageName, result); + return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result); + } + + /** + * iptables -F + * iptables -X + */ + private void cleanIptables() { + // Delete all rules in chain or all chains + CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand("shell iptables -F") + .build() + ); + // Delete user-defined chain + CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand("shell iptables -X") + .build() + ); + } + + /** + * start Tcpdump + * iptables option + * tcpdump pcap + */ + public CommandResult startTcpdump(String packageName) { + // clean iptables conf + this.cleanIptables(); + + String taskId = T.IdUtil.fastSimpleUUID(); + String pcapFilePath = "/data/local/tmp/" + taskId + ".pcap"; + if (T.StrUtil.isNotEmpty(packageName)) { + log.info("[startTcpdump] [capture app package] [pkg: {}]", packageName); + String dumpsysResult = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand("shell dumpsys package " + packageName) + .build() + ); + String[] lines = dumpsysResult.split("\\n"); + String userId = Arrays.stream(lines) + .filter(s -> T.StrUtil.contains(s, "userId=")) + .findFirst() + .map(s -> T.StrUtil.trim(s).replaceAll("userId=", "")) + .orElseThrow(() -> new APIException("Not found userId by package name. package name: " + packageName)); + + CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell iptables -A OUTPUT -m owner --uid-owner %s -j CONNMARK --set-mark %s", userId, userId)) + .build()); + CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell iptables -A INPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId)) + .build()); + CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell iptables -A OUTPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId)) + .build()); + + String ruleList = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand("shell iptables -L") + .build()); + log.info("[startTcpdump] [iptables -L] [result: {}]", ruleList); + + CommandExec.execForProcess(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell tcpdump -i nflog:%s -w %s &", userId, pcapFilePath)) + .build()); + } else { + log.info("[startTcpdump] [capture all package]"); + CommandExec.execForProcess(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell tcpdump -w %s &", pcapFilePath)) + .build()); + } + + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $2}' \"", taskId)) + .build()); + log.info("[startTcpdump] [taskId: {}] [tcpdump pid: {}]", taskId, result); + return new CommandResult(T.StrUtil.isNotEmpty(result) ? 0 : 1, taskId); + } + + /** + * stop tcpdump + * kill -INT {pid} + */ + public CommandResult stopTcpdump(String id) { + String result = CommandExec.exec(AdbCommandBuilder.builder() + .serial(this.getSerial()) + .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $2}' | xargs kill -INT \"", id)) + .build()); + log.info("[stopTcpdump] [id: {}] [result: {}]", id, result); + return new CommandResult(T.StrUtil.isEmpty(result) ? 0 : 1, result); + } + + + private synchronized ExecutorService getThreadPool() { + if (threadPool == null) { + threadPool = new ThreadPoolExecutor( + 5, + 10, + 30, + TimeUnit.SECONDS, + new ArrayBlockingQueue<Runnable>(10000), + new NamedThreadFactory("API-", true)); + } + return threadPool; + } + + class CommandExec { + public static String exec(List<String> command) { + String str = T.RuntimeUtil.execForStr(T.CharsetUtil.CHARSET_UTF_8, command.stream().toArray(String[]::new)); + return str.stripTrailing(); + } + + public static Process execForProcess(List<String> command) { + Process process = T.RuntimeUtil.exec(command.stream().toArray(String[]::new)); + return process; + } + } +}
\ No newline at end of file diff --git a/src/main/java/net/geedge/api/util/ApkInfo.java b/src/main/java/net/geedge/api/util/ApkInfo.java new file mode 100644 index 0000000..8806d4e --- /dev/null +++ b/src/main/java/net/geedge/api/util/ApkInfo.java @@ -0,0 +1,169 @@ +package net.geedge.api.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ApkInfo { + + public static final String APPLICATION_ICON_120 = "application-icon-120"; + public static final String APPLICATION_ICON_160 = "application-icon-160"; + public static final String APPLICATION_ICON_240 = "application-icon-240"; + public static final String APPLICATION_ICON_320 = "application-icon-320"; + + // 所需设备属性 + private List<String> features; + // 图标 + private String icon; + // 各分辨率下图标路径 + private Map<String, String> icons; + // 应用程序名 + private String label; + // 入口Activity + private String launchableActivity; + // 支持的Android平台最低版本号 + private String minSdkVersion; + // 主包名 + private String packageName; + // 支持的SDK版本 + private String sdkVersion; + // Apk文件大小(字节) + private long size; + // 目标SDK版本 + private String targetSdkVersion; + // 所需权限 + private List<String> usesPermissions; + // 内部版本号 + private String versionCode; + // 外部版本号 + private String versionName; + + public ApkInfo() { + this.features = new ArrayList<>(); + this.icons = new HashMap<>(); + this.usesPermissions = new ArrayList<>(); + } + + public List<String> getFeatures() { + return features; + } + + public void setFeatures(List<String> features) { + this.features = features; + } + + public void addToFeatures(String feature) { + this.features.add(feature); + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public Map<String, String> getIcons() { + return icons; + } + + public void setIcons(Map<String, String> icons) { + this.icons = icons; + } + + public void addToIcons(String key, String value) { + this.icons.put(key, value); + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getLaunchableActivity() { + return launchableActivity; + } + + public void setLaunchableActivity(String launchableActivity) { + this.launchableActivity = launchableActivity; + } + + public String getMinSdkVersion() { + return minSdkVersion; + } + + public void setMinSdkVersion(String minSdkVersion) { + this.minSdkVersion = minSdkVersion; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getSdkVersion() { + return sdkVersion; + } + + public void setSdkVersion(String sdkVersion) { + this.sdkVersion = sdkVersion; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getTargetSdkVersion() { + return targetSdkVersion; + } + + public void setTargetSdkVersion(String targetSdkVersion) { + this.targetSdkVersion = targetSdkVersion; + } + + public List<String> getUsesPermissions() { + return usesPermissions; + } + + public void setUsesPermissions(List<String> 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; + +/** + * 返回数据 + * <p> + * 错误码、错误内容统一在枚举类RCode中定义, 错误码格式见RCode注释,错误码内容必须用英文,作为国际化的code 自定义的错误类型必须加注释 + */ +public class R extends HashMap<String, Object> { + + 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<String, Object>()); + if (!(data instanceof Map)) { + throw new APIException("data put error"); + } + ((Map<String, Object>) 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生成器工具类,此工具类中主要封装: + * + * <pre> + * 1. 唯一性ID生成器:UUID、ObjectId(MongoDB)、Snowflake + * </pre> + * + * <p> + * 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工具类<br> + * IO工具类只是辅助流的读写,并不负责关闭流。原因是流可能被多次读写,读写关闭后容易造成问题。 + * + * @author xiaoleilu + */ + public static class IoUtil extends cn.hutool.core.io.IoUtil { + } + /** + * URL(Uniform Resource Locator)统一资源定位符相关工具类 + * + * <p> + * 统一资源定位符,描述了一台特定服务器上某资源的特定位置。 + * </p> + * URL组成: + * + * <pre> + * 协议://主机名[:端口]/ 路径/[:参数] [?查询]#Fragment + * protocol :// hostname[:port] / path / [:parameters][?query]#fragment + * </pre> + * + * @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<String> 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 |
