diff --git a/muyun-boot/src/test/java/net/ximatai/muyun/test/fileserver/TestFileCRUD.java b/muyun-boot/src/test/java/net/ximatai/muyun/test/fileserver/TestFileCRUD.java new file mode 100644 index 00000000..e8fa1d4b --- /dev/null +++ b/muyun-boot/src/test/java/net/ximatai/muyun/test/fileserver/TestFileCRUD.java @@ -0,0 +1,98 @@ +package net.ximatai.muyun.test.fileserver; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.vertx.core.json.JsonObject; +import jakarta.inject.Inject; +import net.ximatai.muyun.fileserver.FileInfoEntity; +import net.ximatai.muyun.fileserver.FileServerConfig; +import net.ximatai.muyun.fileserver.IFileService; +import net.ximatai.muyun.test.testcontainers.PostgresTestResource; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@QuarkusTest +@QuarkusTestResource(value = PostgresTestResource.class, restrictToAnnotatedClass = true) +public class TestFileCRUD { + + @Inject + IFileService service; + + @Inject + FileServerConfig config; + + @Test + void testCRUD() throws IOException, InterruptedException { + // save() + int fileNameInt = getRandomInt(); + String fileName = String.valueOf(fileNameInt) + ".txt"; + File tempFile = File.createTempFile(fileName, ".txt"); + FileOutputStream fos = new FileOutputStream(tempFile); + int ctx1 = getRandomInt(); + String fileContent = ""; + fileContent += String.valueOf(ctx1 + "\n"); + int ctx2 = getRandomInt(); + fileContent += String.valueOf(ctx2); + fos.write(fileContent.getBytes()); + fos.close(); + String id = service.save(tempFile, fileName); + String filePathWithN = config.uploadPath() + id + "-n"; + String filePathWithO = config.uploadPath() + id + "-o"; + Path pathN = Paths.get(filePathWithN); + Path pathO = Paths.get(filePathWithO); + Thread.sleep(1000); + assertTrue(Files.exists(pathN)); + assertTrue(Files.exists(pathO)); + String name = Files.readString(pathN); + String content = Files.readString(pathO); + assertEquals(fileName, name); + assertEquals(fileContent, content); + + // get() + File file = service.get(id); + if (file == null) System.out.println("file is null"); + assert file != null; + String newFileName = file.getName(); + assertEquals(fileName, newFileName); + String newFileContent = Files.readString(Paths.get(file.getPath())); + assertEquals(fileContent, newFileContent); + + // info + FileInfoEntity entity = service.info(id); + JsonObject jsonObject = entity.toJson(); + String infoName = jsonObject.getString("name"); + long infoSize = jsonObject.getLong("size"); + String infoSuffix = jsonObject.getString("suffix"); + assertEquals(fileName, infoName); + assertEquals(fileContent.length(), infoSize); + assertEquals("txt", infoSuffix); + + // delete() + boolean isDeleted = service.delete(id); + assertTrue(isDeleted); + boolean isDeleted2 = file.delete(); + assertTrue(isDeleted2); + } + + /** + * @return 返回一个随机的正数 + */ + int getRandomInt() { + Random rand = new Random(); + int num = rand.nextInt(); + while (num < 1000000000) { + num = rand.nextInt(); + } + return num; + } +} diff --git a/muyun-boot/src/test/java/net/ximatai/muyun/test/fileserver/TestFileServer.java b/muyun-boot/src/test/java/net/ximatai/muyun/test/fileserver/TestFileServer.java index 1d389b06..41fbe77a 100644 --- a/muyun-boot/src/test/java/net/ximatai/muyun/test/fileserver/TestFileServer.java +++ b/muyun-boot/src/test/java/net/ximatai/muyun/test/fileserver/TestFileServer.java @@ -23,7 +23,7 @@ public class TestFileServer { // 文件名 String fileName; - String uid; + String id; // 文件内容 String fileContent = ""; // 临时文件 @@ -49,19 +49,19 @@ void testFileProcess() { Response response = given() .multiPart("file", tempFile) .when() - .post("/fileServer/form") + .post("/fileServer/upload") .then() .log().all() .statusCode(200) .extract() .response(); - uid = response.getBody().asString(); + id = response.getBody().asString(); // 下载文件 Response response2 = given() .when() - .get("/fileServer/download/" + uid) + .get("/fileServer/download/" + id) .then() .log().all() .statusCode(200) @@ -75,7 +75,7 @@ void testFileProcess() { // 读取文件info Response response3 = given() .when() - .get("/fileServer/info/" + uid) + .get("/fileServer/info/" + id) .then() .log().all() .statusCode(200) @@ -89,7 +89,7 @@ void testFileProcess() { @AfterEach public void tearDown() { - String deleteUrl = "/fileServer/delete/" + uid; + String deleteUrl = "/fileServer/delete/" + id; given() .when() .get(deleteUrl) diff --git a/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/FileInfoEntity.java b/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/FileInfoEntity.java index 632a2fe9..b7ffbbb2 100644 --- a/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/FileInfoEntity.java +++ b/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/FileInfoEntity.java @@ -1,20 +1,22 @@ package net.ximatai.muyun.fileserver; +import io.vertx.core.json.JsonObject; + public class FileInfoEntity { String name; long size; String suffix; - String uid; + String id; String time; public FileInfoEntity() { } - public FileInfoEntity(String name, long size, String suffix, String uid, String time) { + public FileInfoEntity(String name, long size, String suffix, String id, String time) { this.name = name; this.size = size; this.suffix = suffix; - this.uid = uid; + this.id = id; this.time = time; } @@ -42,12 +44,12 @@ public void setSuffix(String suffix) { this.suffix = suffix; } - public String getUid() { - return uid; + public String getId() { + return id; } - public void setUid(String uid) { - this.uid = uid; + public void setId(String id) { + this.id = id; } public String getTime() { @@ -57,4 +59,14 @@ public String getTime() { public void setTime(String time) { this.time = time; } + + public JsonObject toJson() { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", name) + .put("size", size) + .put("suffix", suffix) + .put("id", id) + .put("time", time); + return jsonObject; + } } diff --git a/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/FileService.java b/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/FileService.java index 2078b42e..58536a7f 100644 --- a/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/FileService.java +++ b/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/FileService.java @@ -1,8 +1,9 @@ package net.ximatai.muyun.fileserver; +import io.vertx.core.Future; +import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.FileUpload; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; @@ -20,10 +21,11 @@ import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; -//TODO 抽象一个 IFileService 接口,对Java层暴露相关方法 @ApplicationScoped -public class FileService { +public class FileService implements IFileService { final Logger logger = LoggerFactory.getLogger(getClass()); @@ -35,77 +37,64 @@ public class FileService { @Inject Vertx vertx; - // TODO 代码不够优雅,尤其是最后一行 return private String getRootPath() { String rootPath = config.pagePath(); - rootPath = rootPath.startsWith("/") ? rootPath : "/" + rootPath; - return rootPath.endsWith("/") ? rootPath : rootPath + "/"; + if (!rootPath.startsWith("/")) { + rootPath = "/" + rootPath; + } + if (!rootPath.endsWith("/")) { + rootPath = rootPath + "/"; + } + return rootPath; } void init(@Observes Router router, Vertx vertx) { router.get(getRootPath() + "index").handler(this::indexFunc); - router.post(getRootPath() + "form").handler(this::form); - router.get(getRootPath() + "download/:fileUid").handler(this::download); - router.get(getRootPath() + "delete/:uid").handler(this::delete); - router.get(getRootPath() + "info/:uid").handler(this::info); + router.post(getRootPath() + "upload").handler(this::upload); + router.get(getRootPath() + "download/:id").handler(this::download); + router.get(getRootPath() + "delete/:id").handler(this::delete); + router.get(getRootPath() + "info/:id").handler(this::info); } - //TODO 改成 """ """ 的方式存放字符串 // @Route(path = "/fileServer/index", methods = Route.HttpMethod.GET) private void indexFunc(RoutingContext ctx) { ctx.response() .putHeader("content-type", "text/html") .end( - // "
\n" - "\n" - + "
\n" - + " \n" - + " \n" - + "
\n" - + "
\n" - + " \n" - + "
" - + "
" + """ +
+
+ + +
+
+ +
+
+ """ ); } - // TODO 要知道这是对 前端开放的上传接口,所以叫 form 不合适,正常应该叫 upload // @Route(path = "/fileServer/form", methods = Route.HttpMethod.POST) - private void form(RoutingContext ctx) { - + private void upload(RoutingContext ctx) { // 支持分块传输编码 ctx.response().setChunked(true); for (FileUpload f : ctx.fileUploads()) { - // 原来之前面向http的处理逻辑 - /* - // 获取文件名、文件大小、uid - String uploadedFileName = f.uploadedFileName(); - originalFileName = f.fileName(); - long fileSize = f.size(); - String uid = "bsy-" + uploadedFileName.split("\\\\")[2]; - String fileNameUid = suffixFileNameWithN(uid); - String fileContextUid = suffixFileNameWithO(uid); - vertx.fileSystem().writeFile(config.uploadPath() + fileNameUid, Buffer.buffer(originalFileName)); - vertx.fileSystem().copy(uploadedFileName, config.uploadPath() + fileContextUid); - vertx.fileSystem().delete(uploadedFileName); - */ - - // 面向Java的文件上传逻辑 String uploadedFileName = f.uploadedFileName(); originalFileName = f.fileName(); File file = new File(uploadedFileName); - String uid = save(file); - ctx.response().write(uid); + String id = save(file, originalFileName); + ctx.response().write(id); } ctx.response().end(); } - // @Route(path = "/fileServer/download/:fileUid", methods = Route.HttpMethod.GET) + // @Route(path = "/fileServer/download/:id", methods = Route.HttpMethod.GET) private void download(RoutingContext ctx) { - String fileUid = ctx.pathParam("fileUid"); - File fileObtained = obtain(fileUid); + String id = ctx.pathParam("id"); + File fileObtained = get(id); // 发送文件到客户端 - String nameFile = suffixFileNameWithN(fileUid); + String nameFile = suffixFileNameWithN(id); String nameFilePath = config.uploadPath() + nameFile; vertx.fileSystem().readFile(nameFilePath, result -> { if (result.succeeded()) { @@ -115,19 +104,19 @@ private void download(RoutingContext ctx) { ctx.response() .putHeader("Content-Disposition", "attachment; filename=" + content) .sendFile(fileObtained.getPath()); + vertx.fileSystem().delete(fileObtained.getPath()); } } else { logger.error("Failed to read file name: " + result.cause()); ctx.fail(result.cause()); } }); - } - // @Route(path = "/fileServer/delete/:uid", methods = Route.HttpMethod.GET) + // @Route(path = "/fileServer/delete/:id", methods = Route.HttpMethod.GET) private void delete(RoutingContext ctx) { - String uid = ctx.pathParam("uid"); - boolean isDeleted = drop(uid); + String id = ctx.pathParam("id"); + boolean isDeleted = delete(id); if (isDeleted) { ctx.response().end("Successfully deleted."); } else { @@ -135,12 +124,26 @@ private void delete(RoutingContext ctx) { } } - // @Route(path = "/fileServer/info/:uid", methods = Route.HttpMethod.GET) private void info(RoutingContext ctx) { - String uid = ctx.pathParam("uid"); - String fileNamePath = suffixFileNameWithN(config.uploadPath() + uid); - String fileContentPath = suffixFileNameWithO(config.uploadPath() + uid); - File file = new File(fileNamePath); + String id = ctx.pathParam("id"); + asyncInfo(id) + .onSuccess(entity -> { + ctx.response() + .putHeader("Content-Type", "application/json") + .end(entity.toJson().toString()); + + }).onFailure(err -> { + logger.error("Failed to get file info: " + err); + ctx.fail(err); + }); + } + + // 异步得到文件信息 + public Future asyncInfo(String id) { + Promise promise = Promise.promise(); + String fileNamePath = suffixFileNameWithN(config.uploadPath() + id); + String fileContentPath = suffixFileNameWithO(config.uploadPath() + id); + File fileName = new File(fileNamePath); File fileContent = new File(fileContentPath); vertx.fileSystem().readFile(fileNamePath, result -> { if (result.succeeded()) { @@ -156,112 +159,95 @@ private void info(RoutingContext ctx) { } catch (IOException e) { logger.error("Failed to read file attributes", e); } - - JsonObject jsonObject = new JsonObject() - .put("name", line) - .put("size", fileContent.length()) - .put("suffix", suffix) - .put("uid", uid) - .put("time", createTime); - ctx.response() - .putHeader("Content-Type", "application/json") - .end(jsonObject.toString()); - + FileInfoEntity entity = new FileInfoEntity(line, fileContent.length(), suffix, id, createTime); + promise.complete(entity); } else { logger.error("Failed to read file name: " + result.cause()); - ctx.fail(result.cause()); + promise.fail(result.cause()); } }); - -// try { -// FileInfoEntity fileInfoEntity = show(uid); -// String name = fileInfoEntity.getName(); -// long size = fileInfoEntity.getSize(); -// String suffix = fileInfoEntity.getSuffix(); -// String time = fileInfoEntity.getTime(); -// JsonObject jsonObject = new JsonObject() -// .put("name", name) -// .put("size", size) -// .put("suffix", suffix) -// .put("uid", uid) -// .put("time", time); -// ctx.response() -// .putHeader("Content-Type", "application/json") -// .end(Json.encodePrettily(jsonObject.toString())); -// }catch (ExecutionException | InterruptedException e){ -// e.printStackTrace(); -// } + return promise.future(); } - // TODO 检查下两个文件都生成了么,内容是否完整,要求需要有匹配的单元测试 + public FileInfoEntity info(String id) { + CompletableFuture completableFuture = new CompletableFuture(); + asyncInfo(id) + .onSuccess(entity -> { + completableFuture.complete(entity); + }) + .onFailure(err -> { + completableFuture.completeExceptionally(err); + }); + try { + return completableFuture.get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + // 保存文件 - public String save(File file) { - String saveUid = generateBsyUid(); + public String save(File file, String originalFileName) { + String saveId = generateBsyUid(); String saveFileName = file.getName(); long saveSize = file.length(); - String saveFileNameUid = suffixFileNameWithN(saveUid); - String saveFileContextUid = suffixFileNameWithO(saveUid); + String saveFileNameUid = suffixFileNameWithN(saveId); + String saveFileContextUid = suffixFileNameWithO(saveId); // 写入文件名 vertx.fileSystem().writeFile(config.uploadPath() + saveFileNameUid, Buffer.buffer(originalFileName)); vertx.fileSystem().copy(file.getAbsolutePath(), config.uploadPath() + saveFileContextUid); vertx.fileSystem().delete(file.getAbsolutePath()); - return saveUid; + return saveId; } - //TODO get\fetch\ 都要比obtain更合适,同时需要考虑文件不存在的情况,还有就是不要把原始File在java层返回, - // 我们不希望获取到File 的java代码可以修改我们的原始文件,所以应该复制一份 - // 准备对应的单元测试 // 获取文件 - public File obtain(String uid) { - String contentFile = suffixFileNameWithO(uid); + public File get(String id) { + String nameFile = suffixFileNameWithN(id); + String contentFile = suffixFileNameWithO(id); + String nameFilePath = config.uploadPath() + nameFile; String contentFilePath = config.uploadPath() + contentFile; - File file = new File(contentFilePath); - return file; + Path pathN = Paths.get(nameFilePath); + Path pathO = Paths.get(contentFilePath); + if (!Files.exists(pathN)) return null; + try { + String name = Files.readString(pathN); + String context = Files.readString(pathO); + String fileBak = config.uploadPath() + name; + File newFile = new File(fileBak); + if (newFile.createNewFile()) { + Files.writeString(Paths.get(fileBak), context); + return newFile; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; } - //TODO delete 更简单,另外就是考虑文件不存在的情况,准备对应的单元测试 // 丢弃服务器端中的文件 - public boolean drop(String uid) { - String deleteNamePath = suffixFileNameWithN(config.uploadPath() + uid); - String deleteContentPath = suffixFileNameWithO(config.uploadPath() + uid); + public boolean delete(String id) { + String deleteNamePath = suffixFileNameWithN(config.uploadPath() + id); + String deleteContentPath = suffixFileNameWithO(config.uploadPath() + id); File fileN = new File(deleteNamePath); File fileO = new File(deleteContentPath); - boolean isDelete1 = fileN.delete(); - boolean isDelete2 = fileO.delete(); - return isDelete1 && isDelete2; + vertx.fileSystem().delete(deleteNamePath, res -> { + if (res.succeeded()) { + logger.info("FileName deleted successfully: " + deleteNamePath); + } else { + logger.error("Failed to delete file: " + deleteNamePath, res.cause()); + } + }); + vertx.fileSystem().delete(deleteContentPath, res -> { + if (res.succeeded()) { + logger.info("FileContent deleted successfully: " + deleteContentPath); + } else { + logger.error("Failed to delete file: " + deleteContentPath, res.cause()); + } + }); + return fileN.exists() || fileO.exists(); } -// public FileInfoEntity show(String uid) throws ExecutionException, InterruptedException { -// String fileNamePath = suffixFileNameWithN(config.uploadPath() + uid); -// String fileContentPath = suffixFileNameWithO(config.uploadPath() + uid); -// File file = new File(fileNamePath); -// File fileContent = new File(fileContentPath); -// CompletableFuture completableFuture = new CompletableFuture(); -// vertx.fileSystem().readFile(fileNamePath, result -> { -// if (result.succeeded()) { -// Buffer buffer = result.result(); -// String line = buffer.toString("UTF-8"); -// String suffix = line.split("\\.")[1]; -// Path path = Paths.get(fileNamePath); -// String createTime = "00:00"; -// try { -// BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); -// FileTime creationTime = attrs.creationTime(); -// createTime = creationTime.toString(); -// } catch (IOException e) { -// logger.error("Failed to read file attributes", e); -// } -// -// FileInfoEntity entity = new FileInfoEntity(line, fileContent.length(), suffix, uid, createTime); -// completableFuture.complete(entity); -// } else { -// logger.error("Failed to read file name: " + result.cause()); -// } -// }); -// -// return (FileInfoEntity) completableFuture.get(); -// } - // uid文件名处理方法 private String suffixFileNameWithN(String fileName) { return fileName + "-n"; diff --git a/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/IFileService.java b/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/IFileService.java new file mode 100644 index 00000000..61674f66 --- /dev/null +++ b/muyun-fileserver/src/main/java/net/ximatai/muyun/fileserver/IFileService.java @@ -0,0 +1,13 @@ +package net.ximatai.muyun.fileserver; + +import java.io.File; + +public interface IFileService { + String save(File file, String originFileName); + + File get(String id); + + boolean delete(String id); + + FileInfoEntity info(String id); +}