diff --git a/DockerFile b/DockerFile index dd920c1..423a8c2 100644 --- a/DockerFile +++ b/DockerFile @@ -1,10 +1,8 @@ FROM eclipse-temurin:21-jdk-jammy MAINTAINER TongTongStudio RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak -RUN mkdir /data -RUN mkdir /data/uploads/ -RUN mkdir /data/uploads/tablet/ -RUN mkdir /data/uploads/tablet/avatar/ + +RUN mkdir -p /data/uploads/ VOLUME /tmp ADD target/*.jar app.jar diff --git a/pom.xml b/pom.xml index 4349ba1..eb9ce29 100644 --- a/pom.xml +++ b/pom.xml @@ -138,6 +138,13 @@ 2.10.1 + + commons-io + commons-io + 2.14.0 + + + com.h2database diff --git a/src/main/java/com/onekeycall/videotablet/config/FilePath.java b/src/main/java/com/onekeycall/videotablet/config/FilePath.java index 696b554..e313631 100644 --- a/src/main/java/com/onekeycall/videotablet/config/FilePath.java +++ b/src/main/java/com/onekeycall/videotablet/config/FilePath.java @@ -1,13 +1,42 @@ package com.onekeycall.videotablet.config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; + import java.io.File; public class FilePath { - public static final String UPLOAD_FILE_PATH = "uploadFile"; +// @Value("${file.upload-dir-unix}") + private static final String unixUploadDir = "/data/uploads" ; + +// @Value("${file.upload-dir-windows}") + private static final String windowsUploadDir = "uploadFile"; + + private static Logger logger = LoggerFactory.getLogger(FilePath.class); + + public static final String TABLET_PATH = "tablet"; public static final String AVATAR_PATH = "avatar"; + public static final String APK_ICON_PATH = "apkIcon"; - public static String getAvatarPath(){ - return UPLOAD_FILE_PATH + File.separator + TABLET_PATH + File.separator + AVATAR_PATH + File.separator; + public static String getRootPath() { + String osName = System.getProperty("os.name"); + logger.info("osName: {}", osName); + if (osName.contains("Windows")) { + String projectPath = System.getProperty("user.dir"); + logger.info("projectPath: {}", projectPath); + return projectPath + File.separator + windowsUploadDir; + } else { + return unixUploadDir; + } + } + + public static String getAvatarPath() { + return getRootPath() + File.separator + TABLET_PATH + File.separator + AVATAR_PATH + File.separator; + } + + public static String getApkIconPath() { + return getRootPath() + File.separator + TABLET_PATH + File.separator + APK_ICON_PATH + File.separator; } } diff --git a/src/main/java/com/onekeycall/videotablet/controller/pub/FileController.java b/src/main/java/com/onekeycall/videotablet/controller/pub/FileController.java new file mode 100644 index 0000000..b84a6db --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/controller/pub/FileController.java @@ -0,0 +1,106 @@ +package com.onekeycall.videotablet.controller.pub; + +import com.onekeycall.videotablet.config.FilePath; +import com.onekeycall.videotablet.entity.ApkIconFileInfo; +import com.onekeycall.videotablet.entity.TabletDefaultSettings; +import com.onekeycall.videotablet.mapper.TabletDefaultSettingsMapper; +import com.onekeycall.videotablet.result.Result; +import com.onekeycall.videotablet.service.ApkIconService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/public") +public class FileController { + Logger logger = LoggerFactory.getLogger(FileController.class); + + @Autowired + private TabletDefaultSettingsMapper tabletDefaultSettingsMapper; + @Autowired + private ApkIconService apkIconService; + + @PostMapping("/tablet/upload_avatar") + public Result uploadAvatar(@RequestParam("file") MultipartFile multipartFile) { + + String avatarPath = new FilePath().getAvatarPath(); + + File fileDir = new File(avatarPath); + if (!fileDir.exists()) { + fileDir.mkdirs(); + } + + String originalFilename = multipartFile.getOriginalFilename(); + logger.info("originalFilename:" + originalFilename); + File file = new File(avatarPath + File.separator + originalFilename); + logger.info("file path = " + file.getAbsolutePath()); + + try { + multipartFile.transferTo(file); + } catch (IOException e) { + logger.error(e.getMessage()); + return Result.error().message("upload avatarPath failed"); + } + + // 检查是否存在默认设置记录 + TabletDefaultSettings tabletDefaultSettings = tabletDefaultSettingsMapper.getDefaultSettings(); + + if (tabletDefaultSettings == null) { + // 如果不存在,则创建新记录 + tabletDefaultSettings = new TabletDefaultSettings(); + tabletDefaultSettings.setDefaultAvatar(originalFilename); + tabletDefaultSettingsMapper.insertDefaultSettings(tabletDefaultSettings); + } else { + // 如果存在,则更新记录 + tabletDefaultSettings.setDefaultAvatar(originalFilename); + tabletDefaultSettingsMapper.updateDefaultSettings(tabletDefaultSettings); + } + + return Result.ok(); + } + + + @GetMapping("/apk_icon/get_url") + public ResponseEntity getApkIconUrl( + @RequestParam("package_name") String packageName) { + + List apkIconFileInfoList = apkIconService.findByPackageName(packageName); + if (apkIconFileInfoList.isEmpty()) { + return new ResponseEntity<>(Result.notFound().message("未找到图标"), HttpStatus.NOT_FOUND); + } + + Optional apkIconFileInfoOptional = apkIconFileInfoList.stream().max(new Comparator() { + @Override + public int compare(ApkIconFileInfo o1, ApkIconFileInfo o2) { + return Long.compare(o1.getVersionCode(), o2.getVersionCode()); + } + }); + ApkIconFileInfo apkIconFileInfo = apkIconFileInfoOptional.get(); + File file = new File(FilePath.getApkIconPath(), apkIconFileInfo.getFileName()); + if (!file.exists()) { + return new ResponseEntity<>(Result.notFound().message("未找到图标"), HttpStatus.NOT_FOUND); + } + + // 包装文件资源 + Resource resource = new FileSystemResource(file); + + // 设置响应头(让浏览器下载而非直接打开) + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + resource.getFilename() + "\"") + .body(resource); + } +} diff --git a/src/main/java/com/onekeycall/videotablet/controller/pub/ManageSnController.java b/src/main/java/com/onekeycall/videotablet/controller/pub/ManageSnController.java index 1896c15..c4a01c8 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/pub/ManageSnController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/pub/ManageSnController.java @@ -35,50 +35,8 @@ public class ManageSnController { @Autowired private DeviceSnService deviceSnService; @Autowired - private TabletDefaultSettingsMapper tabletDefaultSettingsMapper; - @Autowired private TabletDefaultSettingsRepository tabletDefaultSettingsRepository; - @PostMapping("/tablet/upload_avatar") - public Result uploadAvatar(@RequestParam("file") MultipartFile multipartFile) { - String projectPath = System.getProperty("user.dir"); - - String avatarPath = projectPath + File.separator + FilePath.getAvatarPath(); - - File fileDir = new File(avatarPath); - if (!fileDir.exists()) { - fileDir.mkdirs(); - } - - String originalFilename = multipartFile.getOriginalFilename(); - logger.info("originalFilename:" + originalFilename); - File file = new File(avatarPath + File.separator + originalFilename); - logger.info("file path = " + file.getAbsolutePath()); - - try { - multipartFile.transferTo(file); - } catch (IOException e) { - logger.error(e.getMessage()); - return Result.error().message("upload avatarPath failed"); - } - - // 检查是否存在默认设置记录 - TabletDefaultSettings tabletDefaultSettings = tabletDefaultSettingsMapper.getDefaultSettings(); - - if (tabletDefaultSettings == null) { - // 如果不存在,则创建新记录 - tabletDefaultSettings = new TabletDefaultSettings(); - tabletDefaultSettings.setDefaultAvatar(originalFilename); - tabletDefaultSettingsMapper.insertDefaultSettings(tabletDefaultSettings); - } else { - // 如果存在,则更新记录 - tabletDefaultSettings.setDefaultAvatar(originalFilename); - tabletDefaultSettingsMapper.updateDefaultSettings(tabletDefaultSettings); - } - - return Result.ok(); - } - @PostMapping("/add_sn") public Result addSn( @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, diff --git a/src/main/java/com/onekeycall/videotablet/controller/sn/DevicesController.java b/src/main/java/com/onekeycall/videotablet/controller/sn/DevicesController.java index ba28cec..6ed5528 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/sn/DevicesController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/sn/DevicesController.java @@ -1,18 +1,27 @@ package com.onekeycall.videotablet.controller.sn; +import com.onekeycall.videotablet.config.FilePath; +import com.onekeycall.videotablet.entity.ApkIconFileInfo; import com.onekeycall.videotablet.entity.Contact; import com.onekeycall.videotablet.entity.DeviceInfo; import com.onekeycall.videotablet.entity.DeviceLocation; import com.onekeycall.videotablet.result.Result; +import com.onekeycall.videotablet.service.ApkIconService; import com.onekeycall.videotablet.service.ContactService; import com.onekeycall.videotablet.service.DeviceLocationService; import com.onekeycall.videotablet.service.DeviceSnService; +import com.onekeycall.videotablet.utils.HashUtils; import com.onekeycall.videotablet.utils.JwtUtil; import com.onekeycall.videotablet.utils.TextUtils; import jakarta.validation.Valid; +import org.apache.commons.io.FilenameUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.util.Date; import java.util.List; @@ -27,7 +36,10 @@ public class DevicesController { private DeviceLocationService deviceLocationService; @Autowired private ContactService contactService; + @Autowired + private ApkIconService apkIconService; + Logger logger = LoggerFactory.getLogger(DevicesController.class); @PostMapping("/update_location") public Result updateLocation( @@ -98,4 +110,50 @@ public class DevicesController { } + @PostMapping("/upload_apk_icon") + public Result uploadApkIcon( + @RequestPart(value = "file") MultipartFile file, + @RequestParam(value = "package_name") String packageName, + @RequestParam(value = "version_code") Long versionCode, + @RequestParam(value = "md5") String md5 + ) throws Exception { + + String iconPath = FilePath.getApkIconPath(); + logger.info("uploadApkIcon, iconPath: {}", iconPath); + File fileDir = new File(iconPath); + if (!fileDir.exists()) { + fileDir.mkdirs(); + } + + String fileMd5 = HashUtils.calculateMultipartFileMd5(file); + + if (!fileMd5.equals(md5)) { + return Result.error().message("file md5 not match"); + } + + logger.info("uploadApkIcon, fileMd5: {}", fileMd5); + if (apkIconService.existsByPackageNameAndMd5(packageName, md5)) { + return Result.error().message("apk icon already exists"); + } + + String originName = file.getOriginalFilename(); + String fileExtension = FilenameUtils.getExtension(originName); + if (TextUtils.isEmpty(fileExtension)) { + return Result.error().message("file extension is empty"); + } + String fileName = packageName + "_" + md5 + "." + fileExtension; + File destFile = new File(fileDir, fileName); + file.transferTo(destFile); + + ApkIconFileInfo apkIconFileInfo = new ApkIconFileInfo(); + apkIconFileInfo.setPackageName(packageName); + apkIconFileInfo.setVersionCode(versionCode); + apkIconFileInfo.setMd5(md5); + apkIconFileInfo.setOriginFileName(originName); + apkIconFileInfo.setFileName(fileName); + apkIconFileInfo.setFilSize(file.getSize()); + apkIconService.save(apkIconFileInfo); + + return Result.ok(); + } } diff --git a/src/main/java/com/onekeycall/videotablet/entity/ApkIconFileInfo.java b/src/main/java/com/onekeycall/videotablet/entity/ApkIconFileInfo.java new file mode 100644 index 0000000..ff80e2e --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/entity/ApkIconFileInfo.java @@ -0,0 +1,40 @@ +package com.onekeycall.videotablet.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Entity +@Table(name = "devices_apk_icon") +public class ApkIconFileInfo { + + public ApkIconFileInfo() { + } + + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @Column(name = "id", unique = true, nullable = false) + private Long id; + + @NotBlank(message = "包名不能为空") + @Column(name = "package_name", nullable = false) + private String packageName; + + @NotBlank(message = "版本号不能为空") + @Column(name = "version_code", nullable = false) + private Long versionCode; + + @Column(name = "md5", nullable = false) + private String md5; + + @Column(name = "origin_file_name", nullable = false) + private String originFileName; + + @Column(name = "file_name", nullable = false) + private String fileName; + + @Column(name = "fil_size", nullable = false) + private Long filSize; + +} diff --git a/src/main/java/com/onekeycall/videotablet/repository/ApkIconRepository.java b/src/main/java/com/onekeycall/videotablet/repository/ApkIconRepository.java new file mode 100644 index 0000000..caa75e3 --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/repository/ApkIconRepository.java @@ -0,0 +1,18 @@ +package com.onekeycall.videotablet.repository; + +import com.onekeycall.videotablet.entity.ApkIconFileInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ApkIconRepository extends JpaRepository { + + boolean existsByPackageNameAndMd5(String packageName, String md5); + + boolean existsByPackageNameAndVersionCode(String packageName, Long versionCode); + + List findByPackageNameAndMd5(String packageName, String md5); + + List findByPackageName(String packageName); + +} diff --git a/src/main/java/com/onekeycall/videotablet/service/ApkIconService.java b/src/main/java/com/onekeycall/videotablet/service/ApkIconService.java new file mode 100644 index 0000000..6ebd4ec --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/service/ApkIconService.java @@ -0,0 +1,30 @@ +package com.onekeycall.videotablet.service; + +import com.onekeycall.videotablet.entity.ApkIconFileInfo; +import com.onekeycall.videotablet.repository.ApkIconRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ApkIconService { + @Autowired + private ApkIconRepository apkIconRepository; + + public void save(ApkIconFileInfo apkIconFileInfo) { + apkIconRepository.save(apkIconFileInfo); + } + + public boolean existsByPackageNameAndMd5(String packageName, String md5) { + return apkIconRepository.existsByPackageNameAndMd5(packageName, md5); + } + + public List findByPackageNameAndMd5(String packageName, String md5) { + return apkIconRepository.findByPackageNameAndMd5(packageName, md5); + } + + public List findByPackageName(String packageName) { + return apkIconRepository.findByPackageName(packageName); + } +} diff --git a/src/main/java/com/onekeycall/videotablet/utils/HashUtils.java b/src/main/java/com/onekeycall/videotablet/utils/HashUtils.java index ae19141..4e2f2db 100644 --- a/src/main/java/com/onekeycall/videotablet/utils/HashUtils.java +++ b/src/main/java/com/onekeycall/videotablet/utils/HashUtils.java @@ -1,5 +1,7 @@ package com.onekeycall.videotablet.utils; +import org.springframework.web.multipart.MultipartFile; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -8,6 +10,21 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class HashUtils { + + public static String calculateMultipartFileMd5(MultipartFile file) throws NoSuchAlgorithmException, IOException { + MessageDigest digest = MessageDigest.getInstance("MD5"); + // 使用try-with-resources自动管理输入流 + try (InputStream inputStream = file.getInputStream()) { + byte[] buffer = new byte[4096]; // 统一缓冲区大小为4KB + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + digest.update(buffer, 0, bytesRead); + } + } + byte[] hashBytes = digest.digest(); + return bytesToHex(hashBytes); // 复用现有工具方法 + } + public static String getFileMD5(File file) throws NoSuchAlgorithmException, IOException { MessageDigest md = MessageDigest.getInstance("MD5"); FileInputStream fis = new FileInputStream(file); diff --git a/src/main/resources/application-debug.properties b/src/main/resources/application-debug.properties index 9bad4e4..c8f7365 100644 --- a/src/main/resources/application-debug.properties +++ b/src/main/resources/application-debug.properties @@ -60,4 +60,7 @@ logging.level.com.onekeycall.videotablet.filter=DEBUG logging.level.org.springframework.security=DEBUG mybatis.type-aliases-package=com.onekeycall.videotablet.entity -mybatis.mapperLocations=classpath:mapper/*.xml \ No newline at end of file +mybatis.mapperLocations=classpath:mapper/*.xml + +file.upload-dir-unix=/data/uploads +file.upload-dir-windows=uploadFile \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 81956e5..0cfa50f 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -60,4 +60,7 @@ logging.level.com.onekeycall.videotablet.filter=DEBUG logging.level.org.springframework.security=DEBUG mybatis.type-aliases-package=com.onekeycall.videotablet.entity -mybatis.mapperLocations=classpath:mapper/*.xml \ No newline at end of file +mybatis.mapperLocations=classpath:mapper/*.xml + +file.upload-dir-unix=/data/uploads +file.upload-dir-windows=uploadFile \ No newline at end of file diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 81956e5..ad4ee0c 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -60,4 +60,7 @@ logging.level.com.onekeycall.videotablet.filter=DEBUG logging.level.org.springframework.security=DEBUG mybatis.type-aliases-package=com.onekeycall.videotablet.entity -mybatis.mapperLocations=classpath:mapper/*.xml \ No newline at end of file +mybatis.mapperLocations=classpath:mapper/*.xml + +file.upload-dir-unix=/data/uploads +file.upload-dir-windows=uploadFile