diff --git a/docker/Dockerfile b/docker/Dockerfile index 35937a71..f208c46c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,22 +3,31 @@ FROM nvidia/cuda:11.7.1-base-ubuntu22.04 AS base # 设置非交互式安装,避免 tzdata 等包的配置暂停 ENV DEBIAN_FRONTEND=noninteractive +COPY docker/mxcad_x86_64.zip /jmalcloud/ + # 安装 wget 和 tesseract,并配置时区和 locales RUN apt-get update && \ - apt-get install -y --no-install-recommends wget locales tesseract-ocr p7zip-full unrar libheif-examples && \ + apt-get install -y --no-install-recommends wget unzip locales tesseract-ocr p7zip-full unrar libheif-examples && \ locale-gen en_US.UTF-8 && \ update-locale LANG=en_US.UTF-8 && \ # 下载并安装 jellyfin-ffmpeg ARCH=$(dpkg --print-architecture) && \ wget https://repo.jellyfin.org/files/ffmpeg/ubuntu/latest-5.x/${ARCH}/jellyfin-ffmpeg5_5.1.4-3-jammy_${ARCH}.deb && \ dpkg -i jellyfin-ffmpeg5_5.1.4-3-jammy_${ARCH}.deb || apt-get install -fy && \ + # 安装 mxcad + unzip -o /jmalcloud/mxcad_x86_64.zip -d /usr/local/ && \ + rm -f /jmalcloud/mxcad_x86_64.zip && \ + rm -rf /usr/local/__MACOSX/ && \ + mv /usr/local/x86_64/ /usr/local/mxcad && \ + chmod -R 777 /usr/local/mxcad/mxcadassembly && \ + chmod -R 777 ./mx/so/* && \ + cp -r -f ./mx/locale /usr/local/share/locale && \ # 卸载 wget 并清理下载的文件和APT缓存 - rm -f jellyfin-ffmpeg5_5.1.4-3-jammy_${ARCH}.deb && \ - apt-get remove -y wget && \ + apt-get remove -y wget unzip && \ apt-get clean && \ + rm -f jellyfin-ffmpeg5_5.1.4-3-jammy_${ARCH}.deb && \ rm -rf /var/lib/apt/lists/* - # 将/usr/lib/jellyfin-ffmpeg添加到PATH ENV PATH=/usr/lib/jellyfin-ffmpeg:$PATH @@ -41,12 +50,11 @@ RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN mkdir -p /jmalcloud/files /jmalcloud/tess4j/datapath -ADD docker/ip2region.xdb /jmalcloud/ - -ADD tess4j/datapath/chi_sim.traineddata /jmalcloud/tess4j/datapath/ +COPY docker/ip2region.xdb /jmalcloud/ -ADD target/lib /usr/local/clouddisk-lib +COPY tess4j/datapath/chi_sim.traineddata /jmalcloud/tess4j/datapath/ +COPY target/lib /usr/local/clouddisk-lib # 更新 PATH 和 LD_LIBRARY_PATH ENV PATH="/opt/java/openjdk/bin:${PATH}" diff --git a/docker/mxcad_x86_64.zip b/docker/mxcad_x86_64.zip new file mode 100644 index 00000000..46a84d85 Binary files /dev/null and b/docker/mxcad_x86_64.zip differ diff --git a/src/main/java/com/jmal/clouddisk/controller/rest/FileController.java b/src/main/java/com/jmal/clouddisk/controller/rest/FileController.java index f0ac8675..bd804218 100644 --- a/src/main/java/com/jmal/clouddisk/controller/rest/FileController.java +++ b/src/main/java/com/jmal/clouddisk/controller/rest/FileController.java @@ -243,6 +243,15 @@ public void packageDownload(HttpServletRequest request, HttpServletResponse resp } } + @Operation(summary = "获取dwg文件对应的mxweb文件") + @GetMapping("/view/mxweb/{fileId}") + @Permission("cloud:file:list") + @LogOperatingFun(logType = LogOperation.Type.BROWSE) + public ResponseEntity getMxweb(@PathVariable String fileId) { + Optional file = fileService.getMxweb(fileId); + return file.map(fileService::getObjectResponseEntity).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND).body("找不到该文件")); + } + @Operation(summary = "显示缩略图") @GetMapping("/view/thumbnail") @Permission("cloud:file:list") diff --git a/src/main/java/com/jmal/clouddisk/controller/rest/ShareController.java b/src/main/java/com/jmal/clouddisk/controller/rest/ShareController.java index 39d91def..0835b426 100644 --- a/src/main/java/com/jmal/clouddisk/controller/rest/ShareController.java +++ b/src/main/java/com/jmal/clouddisk/controller/rest/ShareController.java @@ -191,6 +191,15 @@ public ResponseEntity publicThumbnail(String id, Boolean showCover, Http return thumbnail(id, showCover, request); } + @Operation(summary = "获取dwg文件对应的mxweb文件") + @GetMapping("/public/s/view/mxweb/{fileId}/{shareId}") + @LogOperatingFun(logType = LogOperation.Type.BROWSE) + public ResponseEntity publicGetMxweb(HttpServletRequest request, @PathVariable String shareId, @PathVariable String fileId) { + validShare(request, shareId); + Optional file = fileService.getMxweb(fileId); + return file.map(fileService::getObjectResponseEntity).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND).body("找不到该文件")); + } + @Operation(summary = "显示缩略图") @GetMapping("/public/s/view/thumbnail/{filename}") @LogOperatingFun(logType = LogOperation.Type.BROWSE) diff --git a/src/main/java/com/jmal/clouddisk/lucene/LuceneService.java b/src/main/java/com/jmal/clouddisk/lucene/LuceneService.java index 73768aab..dacb9c94 100644 --- a/src/main/java/com/jmal/clouddisk/lucene/LuceneService.java +++ b/src/main/java/com/jmal/clouddisk/lucene/LuceneService.java @@ -341,18 +341,23 @@ private String readFileContent(File file, String fileId) { if (!file.isFile() || file.length() < 1) { return null; } - String type = FileTypeUtil.getType(file); - if ("pdf".equals(type)) { - return readContentService.readPdfContent(file, fileId); - } - if ("epub".equals(type)) { - return readContentService.readEpubContent(file, fileId); - } - if ("ppt".equals(type) || "pptx".equals(type)) { - return readContentService.readPPTContent(file); - } - if ("doc".equals(type) || "docx".equals(type)) { - return readContentService.readWordContent(file); + String type = FileTypeUtil.getType(file).toLowerCase(); + switch (type) { + case "pdf" -> { + return readContentService.readPdfContent(file, fileId); + } + case "dwg" -> { + return readContentService.dwg2mxweb(file, fileId); + } + case "epub" -> { + return readContentService.readEpubContent(file, fileId); + } + case "ppt", "pptx" -> { + return readContentService.readPPTContent(file); + } + case "doc", "docx" -> { + return readContentService.readWordContent(file); + } } if (fileProperties.getSimText().contains(type)) { String charset = UniversalDetector.detectCharset(file); diff --git a/src/main/java/com/jmal/clouddisk/lucene/ReadContentService.java b/src/main/java/com/jmal/clouddisk/lucene/ReadContentService.java index 59d21a4c..fc96e95d 100644 --- a/src/main/java/com/jmal/clouddisk/lucene/ReadContentService.java +++ b/src/main/java/com/jmal/clouddisk/lucene/ReadContentService.java @@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil; import com.jmal.clouddisk.media.VideoProcessService; import com.jmal.clouddisk.ocr.OcrService; +import com.jmal.clouddisk.service.Constants; import com.jmal.clouddisk.service.impl.CommonFileService; import com.jmal.clouddisk.util.FileContentUtil; import lombok.RequiredArgsConstructor; @@ -54,6 +55,22 @@ public class ReadContentService { public final VideoProcessService videoProcessService; + /** + * 将 DWG 文件转换为 MXWeb 文件 + * @param file 文件 + * @param fileId 文件 ID + * @return MXWeb 文件路径 + */ + public String dwg2mxweb(File file, String fileId) { + String username = commonFileService.getUsernameByAbsolutePath(Path.of(file.getAbsolutePath())); + // 生成封面图像 + if (StrUtil.isNotBlank(fileId)) { + String outputName = file.getName() + Constants.MXWEB_SUFFIX; + FileContentUtil.dwgConvert(file.getAbsolutePath(), videoProcessService.getVideoCacheDir(username, fileId), outputName); + } + return null; + } + public String readPdfContent(File file, String fileId) { try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(file))) { diff --git a/src/main/java/com/jmal/clouddisk/media/HeifUtils.java b/src/main/java/com/jmal/clouddisk/media/HeifUtils.java index f67fbf00..3370d1f0 100644 --- a/src/main/java/com/jmal/clouddisk/media/HeifUtils.java +++ b/src/main/java/com/jmal/clouddisk/media/HeifUtils.java @@ -88,7 +88,7 @@ private static ProcessBuilder heifConvert(String filepath, String outputPath) { return processBuilder; } - private static StringBuilder printProcessInfo(Process process) throws IOException { + public static StringBuilder printProcessInfo(Process process) throws IOException { StringBuilder output = new StringBuilder(); try (InputStream inputStream = process.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { diff --git a/src/main/java/com/jmal/clouddisk/service/Constants.java b/src/main/java/com/jmal/clouddisk/service/Constants.java index 6ff8cb98..f69e21f7 100644 --- a/src/main/java/com/jmal/clouddisk/service/Constants.java +++ b/src/main/java/com/jmal/clouddisk/service/Constants.java @@ -76,4 +76,6 @@ private Constants() { } public static final int LOCAL_CHUNK_SIZE = 1024 * 1024; + public static final String MXWEB_SUFFIX = ".mxweb"; + } diff --git a/src/main/java/com/jmal/clouddisk/service/IFileService.java b/src/main/java/com/jmal/clouddisk/service/IFileService.java index 0dab3cdc..3728c9a8 100644 --- a/src/main/java/com/jmal/clouddisk/service/IFileService.java +++ b/src/main/java/com/jmal/clouddisk/service/IFileService.java @@ -185,6 +185,13 @@ public interface IFileService { */ Optional thumbnail(String id, Boolean showCover); + /** + * 获取dwg文件对应的mxweb文件 + * @param id fileId + * @return FileDocument + */ + Optional getMxweb(String id); + /** * 显示缩略图(媒体文件封面) * @param id fileId diff --git a/src/main/java/com/jmal/clouddisk/service/impl/CommonFileService.java b/src/main/java/com/jmal/clouddisk/service/impl/CommonFileService.java index ce022bd0..1acb3679 100644 --- a/src/main/java/com/jmal/clouddisk/service/impl/CommonFileService.java +++ b/src/main/java/com/jmal/clouddisk/service/impl/CommonFileService.java @@ -299,6 +299,10 @@ public void createFolder(UploadApiParamDTO upload) { * @return fileId */ public String createFile(String username, File file, String userId, Boolean isPublic) { + if (CaffeineUtil.hasUploadFileCache(file.getAbsolutePath())) { + return null; + } + log.info("createFile"); if (CharSequenceUtil.isBlank(username)) { return null; } diff --git a/src/main/java/com/jmal/clouddisk/service/impl/FileServiceImpl.java b/src/main/java/com/jmal/clouddisk/service/impl/FileServiceImpl.java index 7abb2b12..30a000fa 100644 --- a/src/main/java/com/jmal/clouddisk/service/impl/FileServiceImpl.java +++ b/src/main/java/com/jmal/clouddisk/service/impl/FileServiceImpl.java @@ -4,7 +4,6 @@ import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.date.TimeInterval; import cn.hutool.core.io.FileTypeUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.file.PathUtil; @@ -477,7 +476,7 @@ public String imgUpload(String baseUrl, String filepath, MultipartFile file) { if (!userService.getDisabledWebp(userLoginHolder.getUserId()) && (!"ico".equals(FileUtil.getSuffix(newFile)))) { fileName += Constants.POINT_SUFFIX_WEBP; } - createFile(username, newFile); + uploadFile(username, newFile); return baseUrl + Paths.get("/file", username, filepath, fileName); } catch (IOException e) { throw new CommonException(ExceptionType.FAIL_UPLOAD_FILE.getCode(), ExceptionType.FAIL_UPLOAD_FILE.getMsg()); @@ -649,6 +648,20 @@ public Optional thumbnail(String id, Boolean showCover) { return Optional.empty(); } + @Override + public Optional getMxweb(String id) { + FileDocument fileDocument = mongoTemplate.findById(id, FileDocument.class, COLLECTION_NAME); + if (fileDocument != null) { + String username = userService.getUserNameById(fileDocument.getUserId()); + File file = Paths.get(videoProcessService.getVideoCacheDir(username,id), fileDocument.getName() + Constants.MXWEB_SUFFIX).toFile(); + if (file.exists()) { + fileDocument.setContent(FileUtil.readBytes(file)); + return Optional.of(fileDocument); + } + } + return Optional.empty(); + } + @Override public Optional coverOfMedia(String id, String username) throws CommonException { FileDocument fileDocument = getFileDocumentById(id); @@ -1082,6 +1095,11 @@ public String createFile(String username, File file) { return createFile(username, file, null, null); } + private String uploadFile(String username, File file) { + CaffeineUtil.setUploadFileCache(file.getAbsolutePath()); + return createFile(username, file, null, null); + } + @Override public void updateFile(String username, File file) { modifyFile(username, file); @@ -1285,7 +1303,7 @@ public ResponseResult addFile(String fileName, Boolean isFolder, St fileIntroVO.setPath(resPath); fileIntroVO.setIsFolder(isFolder); fileIntroVO.setSuffix(FileUtil.extName(fileName)); - String fileId = createFile(username, path.toFile()); + String fileId = uploadFile(username, path.toFile()); fileIntroVO.setId(fileId); return ResultUtil.success(fileIntroVO); } @@ -1484,7 +1502,7 @@ public ResponseResult duplicate(String fileId, String newFilename) { // 复制文件 PathUtil.copyFile(fromFilePath, toFilePath); // 保存文件信息 - createFile(username, toFilePath.toFile()); + uploadFile(username, toFilePath.toFile()); return ResultUtil.success(); } @@ -1626,7 +1644,7 @@ private FileDocument getToFileDocument(UploadApiParamDTO upload, String to) { private void saveFileDocument(FileDocument fileDocument) { File file = getFileByFileDocument(fileDocument); String username = userService.getUserNameById(fileDocument.getUserId()); - createFile(username, file); + uploadFile(username, file); } private ResponseResult ossCopy(FileDocument fileDocumentFrom, FileDocument fileDocumentTo, String from, String to, boolean isMove) { @@ -1718,7 +1736,7 @@ public ResponseResult upload(UploadApiParamDTO upload) throws IOExceptio upload.setContentType(file.getContentType()); upload.setSuffix(FileUtil.extName(filename)); FileUtil.writeFromStream(file.getInputStream(), chunkFile); - createFile(upload.getUsername(), chunkFile); + uploadFile(upload.getUsername(), chunkFile); uploadResponse.setUpload(true); } else { // 上传分片 @@ -1780,7 +1798,7 @@ public ResponseResult newFolder(UploadApiParamDTO upload) throws CommonE if (!dir.exists()) { FileUtil.mkdir(dir); } - return ResultUtil.success(createFile(upload.getUsername(), dir)); + return ResultUtil.success(uploadFile(upload.getUsername(), dir)); } @Override @@ -1898,12 +1916,10 @@ public void deleteOnlyDoc(String username, String currentDirectory, List @Override public ResponseResult delete(String username, String currentDirectory, List fileIds, String operator, boolean sweep) { - TimeInterval interval = new TimeInterval(); username = deleteOss(username, currentDirectory, fileIds, operator); if (username == null) { return ResultUtil.success(); } - log.info("deleteOss: {}ms", interval.intervalMs()); Query query = new Query(); query.addCriteria(Criteria.where("_id").in(fileIds)); List fileDocuments = mongoTemplate.find(query, FileDocument.class, COLLECTION_NAME); @@ -1951,7 +1967,6 @@ public ResponseResult delete(String username, String currentDirectory, L } else { operationTips.setSuccess(false); } - log.info("deleteFile: {}ms", interval.intervalMs()); pushMessage(username, operationTips, Constants.OPERATION_TIPS); return ResultUtil.success(); } diff --git a/src/main/java/com/jmal/clouddisk/service/impl/MultipartUpload.java b/src/main/java/com/jmal/clouddisk/service/impl/MultipartUpload.java index 2565fbae..ef6bbdad 100644 --- a/src/main/java/com/jmal/clouddisk/service/impl/MultipartUpload.java +++ b/src/main/java/com/jmal/clouddisk/service/impl/MultipartUpload.java @@ -119,6 +119,7 @@ public UploadResponse mergeFile(UploadApiParamDTO upload) throws IOException { } PathUtil.move(file, outputFile, true); uploadResponse.setUpload(true); + CaffeineUtil.setUploadFileCache(outputFile.toFile().getAbsolutePath()); commonFileService.createFile(upload.getUsername(), outputFile.toFile(), null, null); return uploadResponse; } diff --git a/src/main/java/com/jmal/clouddisk/util/CaffeineUtil.java b/src/main/java/com/jmal/clouddisk/util/CaffeineUtil.java index 266e2da7..34e2e7e2 100644 --- a/src/main/java/com/jmal/clouddisk/util/CaffeineUtil.java +++ b/src/main/java/com/jmal/clouddisk/util/CaffeineUtil.java @@ -7,6 +7,7 @@ import com.jmal.clouddisk.oss.BucketInfo; import com.jmal.clouddisk.webdav.MyWebdavServlet; import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.nio.file.Path; @@ -22,8 +23,14 @@ * @author jmal */ @Component +@Slf4j public class CaffeineUtil { + /** + * 上传文件缓存, 用于判断文件是否刚刚上传, 避免一些重复的操作 + */ + public static final Cache UPLOAD_FILE_CACHE = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).build(); + /** * 缩略图请求缓存 */ @@ -268,6 +275,18 @@ public static String getOssPath(Path path) { return null; } + public static Boolean hasUploadFileCache(String key) { + Long uploadTime = UPLOAD_FILE_CACHE.getIfPresent(key); + if (uploadTime == null) { + return false; + } + return System.currentTimeMillis() - uploadTime > 5; + } + + public static void setUploadFileCache(String key) { + UPLOAD_FILE_CACHE.put(key, System.currentTimeMillis()); + } + public static Boolean hasThumbnailRequestCache(String id) { return THUMBNAIL_REQUEST_CACHE.get(id, key -> false); } diff --git a/src/main/java/com/jmal/clouddisk/util/FFMPEGUtils.java b/src/main/java/com/jmal/clouddisk/util/FFMPEGUtils.java index b0841d57..e8d52d81 100644 --- a/src/main/java/com/jmal/clouddisk/util/FFMPEGUtils.java +++ b/src/main/java/com/jmal/clouddisk/util/FFMPEGUtils.java @@ -42,7 +42,11 @@ public static void printSuccessInfo(ProcessBuilder processBuilder) { * @return outputPath */ public static String getWaitingForResults(String outputPath, ProcessBuilder processBuilder, Process process) throws IOException, InterruptedException { - boolean finished = process.waitFor(12, TimeUnit.SECONDS); + return getWaitingForResults(outputPath, processBuilder, process, 12); + } + + public static String getWaitingForResults(String outputPath, ProcessBuilder processBuilder, Process process, int waitSeconds) throws InterruptedException, IOException { + boolean finished = process.waitFor(waitSeconds, TimeUnit.SECONDS); try { log.debug("finished: {}", finished); log.debug("exitValue: {}", process.exitValue()); diff --git a/src/main/java/com/jmal/clouddisk/util/FileContentUtil.java b/src/main/java/com/jmal/clouddisk/util/FileContentUtil.java index 21b1efda..23369a9e 100644 --- a/src/main/java/com/jmal/clouddisk/util/FileContentUtil.java +++ b/src/main/java/com/jmal/clouddisk/util/FileContentUtil.java @@ -10,10 +10,17 @@ import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import static com.jmal.clouddisk.util.FFMPEGUtils.getWaitingForResults; @Slf4j public class FileContentUtil { + private static final String mxcadassemblyPath = "/usr/local/mxcad"; + public static void readFailed(File file, IOException e) { log.warn("读取文件内容失败, file: {}, {}", file.getAbsolutePath(), e.getMessage()); } @@ -58,4 +65,48 @@ public static File epubCoverImage(Book book, String outputPath) { return null; } + /** + * 将dwg文件转换为mxweb文件的命令 + * + * @param sourceFile dwg文件绝对路径 + * @param outputPath 输出路径 + * @param outputFile 输出文件名 + */ + public static void dwgConvert(String sourceFile, String outputPath, String outputFile) { + if (!Paths.get(mxcadassemblyPath).toFile().exists()) { + return; + } + try { + ProcessBuilder processBuilder = dwgConvertCommand(sourceFile, outputPath, outputFile); + Process process = processBuilder.start(); + outputPath = getWaitingForResults(outputPath, processBuilder, process, 60); + if (outputPath != null) { + log.info("dwg文件转换成功, outputPath: {}", outputPath); + } + } catch (InterruptedException | IOException e) { + log.error(e.getMessage(), e); + } + } + + /** + * 将dwg文件转换为mxweb文件的命令 + * @param sourceFile dwg文件绝对路径 + * @param outPath 输出路径 + * @param outFile 输出文件名 + * @return ProcessBuilder + */ + private static ProcessBuilder dwgConvertCommand(String sourceFile, String outPath, String outFile) { + // 转换文件路径. + String command = "./mxcadassembly"; + // 转换参数。 + String path = "{'srcpath':'" + sourceFile + "','outpath':'" + outPath + "','outname':'" + outFile + "'}"; + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.redirectErrorStream(true); + List commands = new ArrayList<>(); + commands.add(command); + commands.add(path); + processBuilder.command(commands).directory(new File(mxcadassemblyPath)); + processBuilder.redirectErrorStream(true); + return processBuilder; + } } diff --git a/src/main/java/com/jmal/clouddisk/util/MyFileUtils.java b/src/main/java/com/jmal/clouddisk/util/MyFileUtils.java index 26f08828..96a646b0 100644 --- a/src/main/java/com/jmal/clouddisk/util/MyFileUtils.java +++ b/src/main/java/com/jmal/clouddisk/util/MyFileUtils.java @@ -22,9 +22,9 @@ @Slf4j public class MyFileUtils { - public static List hasContentTypes = Arrays.asList("pdf", "drawio", "mind", "doc", "docx", "xls", "xlsx", "xlsm", "ppt", "pptx", "csv", "tsv", "dotm", "xlt", "xltm", "dot", "dotx", "xlam", "xla", "pages", "epub"); + public static List hasContentTypes = Arrays.asList("pdf", "drawio", "mind", "doc", "docx", "xls", "xlsx", "xlsm", "ppt", "pptx", "csv", "tsv", "dotm", "xlt", "xltm", "dot", "dotx", "xlam", "xla", "pages", "epub", "dwg"); - private MyFileUtils(){ + private MyFileUtils() { }