mirror of https://github.com/halo-dev/halo
				
				
				
			Add thumbnail generation router for image uploads
							parent
							
								
									b1127e3c28
								
							
						
					
					
						commit
						503e9b8c7f
					
				| 
						 | 
				
			
			@ -0,0 +1,156 @@
 | 
			
		|||
package run.halo.app.core.attachment.endpoint;
 | 
			
		||||
 | 
			
		||||
import static java.nio.file.StandardOpenOption.CREATE;
 | 
			
		||||
import static java.nio.file.StandardOpenOption.READ;
 | 
			
		||||
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
 | 
			
		||||
import static java.nio.file.StandardOpenOption.WRITE;
 | 
			
		||||
import static org.imgscalr.Scalr.Method.AUTOMATIC;
 | 
			
		||||
import static org.imgscalr.Scalr.Mode.FIT_TO_WIDTH;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import javax.imageio.ImageIO;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
import org.imgscalr.Scalr;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.core.io.FileSystemResource;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.springframework.web.reactive.function.server.RouterFunction;
 | 
			
		||||
import org.springframework.web.reactive.function.server.RouterFunctions;
 | 
			
		||||
import org.springframework.web.reactive.function.server.ServerResponse;
 | 
			
		||||
import org.springframework.web.reactive.resource.NoResourceFoundException;
 | 
			
		||||
import reactor.core.publisher.Mono;
 | 
			
		||||
import reactor.core.scheduler.Scheduler;
 | 
			
		||||
import reactor.core.scheduler.Schedulers;
 | 
			
		||||
import run.halo.app.core.attachment.AttachmentRootGetter;
 | 
			
		||||
import run.halo.app.core.attachment.ThumbnailSize;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@Component
 | 
			
		||||
class ThumbnailRouters {
 | 
			
		||||
 | 
			
		||||
    private final Path uploadRoot;
 | 
			
		||||
 | 
			
		||||
    private final Path thumbnailRoot;
 | 
			
		||||
 | 
			
		||||
    private final Scheduler thumbnailGeneratingScheduler;
 | 
			
		||||
 | 
			
		||||
    public ThumbnailRouters(AttachmentRootGetter attachmentRoot) {
 | 
			
		||||
        this.uploadRoot = attachmentRoot.get().resolve("upload");
 | 
			
		||||
        this.thumbnailRoot = attachmentRoot.get().resolve("thumbnails");
 | 
			
		||||
        this.thumbnailGeneratingScheduler = Schedulers.newBoundedElastic(
 | 
			
		||||
            // Currently, we only allow 10 concurrent thumbnail generations
 | 
			
		||||
            10,
 | 
			
		||||
            Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE,
 | 
			
		||||
            "thumbnail-generator-"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    RouterFunction<ServerResponse> thumbnailRouter() {
 | 
			
		||||
        return RouterFunctions.route()
 | 
			
		||||
            .GET("/thumbnails/w{width}/upload/{*filename}", serverRequest -> {
 | 
			
		||||
                var width = serverRequest.pathVariable("width");
 | 
			
		||||
                String originalFilename = serverRequest.pathVariable("filename");
 | 
			
		||||
                var fileName = StringUtils.removeStart(originalFilename, "/");
 | 
			
		||||
                if (StringUtils.isBlank(fileName)) {
 | 
			
		||||
                    log.trace("Filename is blank");
 | 
			
		||||
                    return Mono.error(new NoResourceFoundException(fileName));
 | 
			
		||||
                }
 | 
			
		||||
                var size = ThumbnailSize.fromWidth(width);
 | 
			
		||||
                // try to resolve the thumbnail
 | 
			
		||||
                // build thumbnail path
 | 
			
		||||
                var thumbnailPath = thumbnailRoot.resolve("w" + size.getWidth())
 | 
			
		||||
                    .resolve(fileName);
 | 
			
		||||
                var thumbnailResource = new FileSystemResource(thumbnailPath);
 | 
			
		||||
                if (thumbnailResource.isReadable()) {
 | 
			
		||||
                    return ServerResponse.ok().bodyValue(thumbnailResource);
 | 
			
		||||
                }
 | 
			
		||||
                // generate for the attachment
 | 
			
		||||
                return Mono.fromCallable(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            var uploadPath = uploadRoot.resolve(fileName);
 | 
			
		||||
                            return generateThumbnail(uploadPath, thumbnailPath, size);
 | 
			
		||||
                        })
 | 
			
		||||
                    .subscribeOn(this.thumbnailGeneratingScheduler)
 | 
			
		||||
                    .switchIfEmpty(Mono.error(() -> new NoResourceFoundException(fileName)))
 | 
			
		||||
                    .map(FileSystemResource::new)
 | 
			
		||||
                    .flatMap(resource -> ServerResponse.ok().bodyValue(resource));
 | 
			
		||||
            })
 | 
			
		||||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Path generateThumbnail(Path attachmentPath, Path thumbnailPath, ThumbnailSize size) {
 | 
			
		||||
        if (!Files.exists(attachmentPath)) {
 | 
			
		||||
            log.trace("Attachment path does not exist: {}", attachmentPath);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        if (Files.exists(thumbnailPath)) {
 | 
			
		||||
            return thumbnailPath;
 | 
			
		||||
        }
 | 
			
		||||
        var attachmentPathString = attachmentPath.toString();
 | 
			
		||||
        synchronized (attachmentPathString) {
 | 
			
		||||
            // double check
 | 
			
		||||
            if (Files.exists(thumbnailPath)) {
 | 
			
		||||
                return thumbnailPath;
 | 
			
		||||
            }
 | 
			
		||||
            if (log.isDebugEnabled()) {
 | 
			
		||||
                log.debug("Generating thumbnail for path: {}, target: {}, size: {}",
 | 
			
		||||
                    attachmentPath, thumbnailPath, size);
 | 
			
		||||
            }
 | 
			
		||||
            var formatName = getFormatName(attachmentPath);
 | 
			
		||||
            if (formatName == null) {
 | 
			
		||||
                log.info("Cannot determine image format for path: {}", attachmentPath);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            try (
 | 
			
		||||
                var inputStream = Files.newInputStream(attachmentPath, READ);
 | 
			
		||||
            ) {
 | 
			
		||||
                var bufferedImage = ImageIO.read(inputStream);
 | 
			
		||||
                if (bufferedImage == null) {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
                if (bufferedImage.getWidth() <= size.getWidth()) {
 | 
			
		||||
                    // if the image is smaller than the thumbnail size, just copy it
 | 
			
		||||
                    Files.createDirectories(thumbnailPath.getParent());
 | 
			
		||||
                    Files.copy(attachmentPath, thumbnailPath);
 | 
			
		||||
                    return thumbnailPath;
 | 
			
		||||
                }
 | 
			
		||||
                // TODO Handle image orientation
 | 
			
		||||
                var thumbnailBufferedImage =
 | 
			
		||||
                    Scalr.resize(bufferedImage, AUTOMATIC, FIT_TO_WIDTH, size.getWidth());
 | 
			
		||||
                Files.createDirectories(thumbnailPath.getParent());
 | 
			
		||||
                try (var outputStream =
 | 
			
		||||
                         Files.newOutputStream(thumbnailPath, CREATE, TRUNCATE_EXISTING, WRITE)
 | 
			
		||||
                ) {
 | 
			
		||||
                    ImageIO.write(thumbnailBufferedImage, formatName, outputStream);
 | 
			
		||||
                }
 | 
			
		||||
                log.info("Generated thumbnail for path: {}, target: {}, size: {}",
 | 
			
		||||
                    attachmentPath, thumbnailPath, size);
 | 
			
		||||
                return thumbnailPath;
 | 
			
		||||
            } catch (IOException e) {
 | 
			
		||||
                log.warn("Failed to generate thumbnail for path: {}", attachmentPath, e);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getFormatName(Path imagePath) {
 | 
			
		||||
        try (var imageInputStream = ImageIO.createImageInputStream(
 | 
			
		||||
            Files.newInputStream(imagePath))
 | 
			
		||||
        ) {
 | 
			
		||||
            var readers = ImageIO.getImageReaders(imageInputStream);
 | 
			
		||||
            if (!readers.hasNext()) {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            var reader = readers.next();
 | 
			
		||||
            return reader.getFormatName();
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            log.warn("Failed to get image format for path: {}", imagePath, e);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue