diff --git a/src/main/java/priv/dino/tus/server/manage/controller/UploadController.java b/src/main/java/priv/dino/tus/server/manage/controller/UploadController.java index 246a973..08184df 100644 --- a/src/main/java/priv/dino/tus/server/manage/controller/UploadController.java +++ b/src/main/java/priv/dino/tus/server/manage/controller/UploadController.java @@ -185,8 +185,7 @@ public Mono> header(@NonNull @PathVariable("id") final Lo parameters = {@Parameter(in = ParameterIn.PATH, name = "id", required = true, description = "上传工作单元的ID", schema = @Schema(type = "string", format = "SnowflakeID")), @Parameter(name = "Upload-Offset", in = ParameterIn.HEADER, required = true, schema = @Schema(type = "integer")), - @Parameter(name = "Content-Length", in = ParameterIn.HEADER, required = true, schema = @Schema(type = "integer")), - @Parameter(name = "Content-Type", example = "application/offset+octet-stream", required = true, schema = @Schema(type = "string"))}) + @Parameter(name = "Content-Type", in = ParameterIn.HEADER, required = true, example = "application/offset+octet-stream", schema = @Schema(type = "string"))}) @RequestMapping( method = {RequestMethod.POST, RequestMethod.PATCH,}, value = {"/{id}"}, @@ -195,8 +194,7 @@ public Mono> header(@NonNull @PathVariable("id") final Lo public Mono> uploadProcess( @NonNull @PathVariable("id") final Long id, @NonNull final ServerHttpRequest request, - @RequestHeader(name = "Upload-Offset") final long offset, - @RequestHeader(name = "Content-Length") final long length + @RequestHeader(name = "Upload-Offset") final long offset ) { request.getHeaders().forEach((k, v) -> log.debug("headers: {} {}", k, v)); @@ -204,7 +202,7 @@ public Mono> uploadProcess( log.debug("SnowflakeID value: " + id); return uploadService - .uploadChunkAndGetUpdatedOffset(id, request.getBody(), offset, length) + .appendFileContent(id, request.getBody(), offset) .log() .map(e -> ResponseEntity .status(NO_CONTENT) diff --git a/src/main/java/priv/dino/tus/server/manage/handler/DownloadHandler.java b/src/main/java/priv/dino/tus/server/manage/handler/DownloadHandler.java index 3840534..146c046 100644 --- a/src/main/java/priv/dino/tus/server/manage/handler/DownloadHandler.java +++ b/src/main/java/priv/dino/tus/server/manage/handler/DownloadHandler.java @@ -34,16 +34,43 @@ public Mono handle(ServerRequest request) { String uploadId = request.pathVariable("uploadId"); return fileRepository.findById(Long.parseLong(uploadId)) - .flatMap(e -> - ServerResponse.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + new String(e.getOriginalName().getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)) + .flatMap(e -> { + String originalFileName = e.getOriginalName(); + String utf8EscapedFileName = escapeUtf8FileName(originalFileName); + String attachment = String.format("attachment; filename=\"%s\"; filename*=utf-8''\"%s\"", utf8EscapedFileName, utf8EscapedFileName); + return ServerResponse.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, attachment) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body((p, a) -> p.writeWith(DataBufferUtils.read(Paths.get(fileDirectory.toString(), uploadId), new DefaultDataBufferFactory(), 4096))) - .doOnNext(a -> log.info("Download file ID: {}", uploadId)) + .doOnNext(a -> log.info("Download file ID: {}", uploadId)); + } ) .switchIfEmpty(ServerResponse.notFound().build()); } + private static String escapeUtf8FileName(String originalFileName) { + byte[] utf8 = originalFileName.getBytes(StandardCharsets.UTF_8); + StringBuilder strBuilder = new StringBuilder(); + for (byte b: utf8) { + char ch = (char)b; + if ((ch >= '0' && ch <='9') || + (ch >= 'a' && ch <='z') || + (ch >= 'A' && ch <='Z') || + ch == '-' || + ch == '_' || + ch == '.' + ) { + strBuilder.append(ch); + continue; + } else if ((b >= 0x00 && b <= 0x1F) || b == 0x7F) { + strBuilder.append('_'); + continue; + } + // RFC5987 规范: 对 URL 中的 UTF-8 文本进行转义编码 + strBuilder.append(String.format("%%%02X", b)); + } + return strBuilder.toString(); + } } diff --git a/src/main/java/priv/dino/tus/server/manage/handler/PatchHandler.java b/src/main/java/priv/dino/tus/server/manage/handler/PatchHandler.java index affa77f..e7d34eb 100644 --- a/src/main/java/priv/dino/tus/server/manage/handler/PatchHandler.java +++ b/src/main/java/priv/dino/tus/server/manage/handler/PatchHandler.java @@ -70,15 +70,11 @@ public Mono handleRequest(ServerRequest serverRequest) { rogueRequest = true; } - if (!contentLength.isPresent()) { - rogueRequest = true; - } - if (rogueRequest) { return ServerResponse.badRequest().build(); } - return uploadService.uploadChunkAndGetUpdatedOffset(Long.valueOf(uploadId),parts,offset.get(),contentLength.get()) + return uploadService.appendFileContent(Long.valueOf(uploadId),parts,offset.orElseGet(() -> 0L)) .log() .flatMap(r -> ServerResponse .noContent() diff --git a/src/main/java/priv/dino/tus/server/manage/router/DownloadRouter.java b/src/main/java/priv/dino/tus/server/manage/router/DownloadRouter.java index 5f1cb97..f64ce5d 100644 --- a/src/main/java/priv/dino/tus/server/manage/router/DownloadRouter.java +++ b/src/main/java/priv/dino/tus/server/manage/router/DownloadRouter.java @@ -49,6 +49,6 @@ public class DownloadRouter { @Bean public RouterFunction route() { return RouterFunctions - .route(GET("/download/{uploadId}").and(accept(MediaType.APPLICATION_JSON)), downloadHandler); + .route(GET("/download/{uploadId}"), downloadHandler); } } diff --git a/src/main/java/priv/dino/tus/server/manage/service/UploadService.java b/src/main/java/priv/dino/tus/server/manage/service/UploadService.java index 63dde4a..8ab3e46 100644 --- a/src/main/java/priv/dino/tus/server/manage/service/UploadService.java +++ b/src/main/java/priv/dino/tus/server/manage/service/UploadService.java @@ -84,42 +84,36 @@ public Mono mergePartialUploads(Long[] extractPartialUploadIds, Optional uploadChunkAndGetUpdatedOffset( + public Mono appendFileContent( final Long id, final Flux parts, - final long offset, - final long length + final long offset ) { Mono fileOne = fileRepository.findById(id) .switchIfEmpty(Mono.error(new GlobalException(HttpStatus.INTERNAL_SERVER_ERROR, "File record not found."))) - .map(e -> this.isValid(e, offset, length)); + .map(this::fileEntityIsValid); return Mono .zip(fileOne,fileStorage.writeChunk(id, parts, offset)) - .flatMap((Tuple2 data) -> this.save(data.getT1(),data.getT2())); + .flatMap((Tuple2 tuple) -> { + File file = tuple.getT1(); + Integer nBytesAppended = tuple.getT2(); + + long contentOffset = file.getContentOffset(); + log.info("[OLD OFFSET] {}", contentOffset); + contentOffset += nBytesAppended; + log.info("[OFFSET] {}", contentOffset); + file.setContentOffset(contentOffset); + file.setLastUploadedChunkNumber(file.getLastUploadedChunkNumber() + 1); + log.debug("File patching: {}", file); + return fileRepository.save(file); + }); } - public Mono save(File file,Integer offset) { - log.info("[OLD OFFSET] {}", file.getContentOffset()); - log.info("[OFFSET] {}", file.getContentOffset() + offset); - file.setContentOffset(file.getContentOffset() + offset); - file.setLastUploadedChunkNumber(file.getLastUploadedChunkNumber() + 1); - log.debug("File patching: {}", file); - return fileRepository.save(file); - } - - private File isValid(File file, long offset, long length) { - if (offset != file.getContentOffset() && checkContentLengthWithCurrentOffset(length, offset, file.getContentLength())) { - throw new GlobalException(HttpStatus.INTERNAL_SERVER_ERROR, "Offset mismatch."); - } + private File fileEntityIsValid(File file) { if (uploadExpiredUtils.checkUploadExpired(file)) { throw new GlobalException(HttpStatus.INTERNAL_SERVER_ERROR, "Upload Expires."); } return file; } - - private boolean checkContentLengthWithCurrentOffset(long contentLength, long offset, long entityLength) { - return contentLength + offset <= entityLength; - } - } diff --git a/src/test/java/priv/dino/tus/server/manage/controllers/UploadControllerTest.java b/src/test/java/priv/dino/tus/server/manage/controller/UploadControllerTest.java similarity index 96% rename from src/test/java/priv/dino/tus/server/manage/controllers/UploadControllerTest.java rename to src/test/java/priv/dino/tus/server/manage/controller/UploadControllerTest.java index d032eff..937a7a9 100644 --- a/src/test/java/priv/dino/tus/server/manage/controllers/UploadControllerTest.java +++ b/src/test/java/priv/dino/tus/server/manage/controller/UploadControllerTest.java @@ -1,10 +1,9 @@ -package priv.dino.tus.server.manage.controllers; +package priv.dino.tus.server.manage.controller; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import priv.dino.tus.server.core.configuration.properties.TusServerProperties; import priv.dino.tus.server.core.util.UploadExpiredUtils; -import priv.dino.tus.server.manage.controller.UploadController; import priv.dino.tus.server.manage.domain.File; import priv.dino.tus.server.manage.repository.FileRepository; import priv.dino.tus.server.manage.service.UploadService; @@ -146,12 +145,12 @@ void uploadProcess() { put("test", Collections.singletonList("test")); }}); Mockito - .when(uploadService.uploadChunkAndGetUpdatedOffset(1L, body, 0, 3)) + .when(uploadService.appendFileContent(1L, body, 0)) .thenReturn(Mono.just(File.builder().contentOffset(3L).build())); final UploadController uploadController = new UploadController(filesRepository, tusServerProperties, uploadService, uploadExpiredUtils); - uploadController.uploadProcess(1L, request, 0, 3) + uploadController.uploadProcess(1L, request, 0) .subscribe(v -> { assertEquals(NO_CONTENT, v.getStatusCode()); assertEquals("3", Objects.requireNonNull(v.getHeaders().get("Upload-Offset")).get(0));