mirror of https://github.com/halo-dev/halo
Expose attachment service to plugin (#3740)
#### What type of PR is this? /kind feature /area core /area plugin #### What this PR does / why we need it: This PR refactor AttachmentEndpoint by extracting `upload`, `delete`, `getPremalink` and `getSharedURL` logic in the endpoint into AttachmentService. Meanwhile, I expose the service to plugin, so that we can use the service in plugin conveniently. #### Special notes for your reviewer: Please confirm that those changes won't influence existing attachment features. #### Does this PR introduce a user-facing change? ```release-note None ```pull/3756/head
parent
d1651aa671
commit
d760d4d362
|
@ -56,7 +56,11 @@ public class Attachment extends AbstractExtension {
|
||||||
@Data
|
@Data
|
||||||
public static class AttachmentStatus {
|
public static class AttachmentStatus {
|
||||||
|
|
||||||
@Schema(description = "Permalink of attachment")
|
@Schema(description = """
|
||||||
|
Permalink of attachment.
|
||||||
|
If it is in local storage, the public URL will be set.
|
||||||
|
If it is in s3 storage, the Object URL will be set.
|
||||||
|
""")
|
||||||
private String permalink;
|
private String permalink;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package run.halo.app.core.extension.attachment;
|
package run.halo.app.core.extension.attachment;
|
||||||
|
|
||||||
|
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;
|
||||||
|
|
||||||
public enum Constant {
|
public enum Constant {
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -14,6 +16,13 @@ public enum Constant {
|
||||||
*/
|
*/
|
||||||
public static final String URI_ANNO_KEY = GROUP + "/uri";
|
public static final String URI_ANNO_KEY = GROUP + "/uri";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not use this key to set external link. You could implement
|
||||||
|
* {@link AttachmentHandler#getPermalink} by your self.
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* @deprecated Use your own group instead.
|
||||||
|
*/
|
||||||
public static final String EXTERNAL_LINK_ANNO_KEY = GROUP + "/external-link";
|
public static final String EXTERNAL_LINK_ANNO_KEY = GROUP + "/external-link";
|
||||||
|
|
||||||
public static final String FINALIZER_NAME = "attachment-manager";
|
public static final String FINALIZER_NAME = "attachment-manager";
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package run.halo.app.core.extension.attachment.endpoint;
|
package run.halo.app.core.extension.attachment.endpoint;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.time.Duration;
|
||||||
import org.pf4j.ExtensionPoint;
|
import org.pf4j.ExtensionPoint;
|
||||||
import org.springframework.http.codec.multipart.FilePart;
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
@ -13,6 +15,44 @@ public interface AttachmentHandler extends ExtensionPoint {
|
||||||
|
|
||||||
Mono<Attachment> delete(DeleteContext context);
|
Mono<Attachment> delete(DeleteContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a shared URL which could be accessed publicly.
|
||||||
|
* 1. If the attachment is in local storage, the permalink will be returned.
|
||||||
|
* 2. If the attachment is in s3 storage, the Presigned URL will be returned.
|
||||||
|
* <p>
|
||||||
|
* Please note that the default implementation is only for back compatibility.
|
||||||
|
*
|
||||||
|
* @param attachment contains detail of attachment.
|
||||||
|
* @param policy is storage policy.
|
||||||
|
* @param configMap contains configuration needed by handler.
|
||||||
|
* @param ttl indicates how long the URL is alive.
|
||||||
|
* @return shared URL which could be accessed publicly. Might be relative URL.
|
||||||
|
*/
|
||||||
|
default Mono<URI> getSharedURL(Attachment attachment,
|
||||||
|
Policy policy,
|
||||||
|
ConfigMap configMap,
|
||||||
|
Duration ttl) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a permalink representing a unique attachment.
|
||||||
|
* If the attachment is in local storage, the permalink will be returned.
|
||||||
|
* If the attachment is in s3 storage, the Object URL will be returned.
|
||||||
|
* <p>
|
||||||
|
* Please note that the default implementation is only for back compatibility.
|
||||||
|
*
|
||||||
|
* @param attachment contains detail of attachment.
|
||||||
|
* @param policy is storage policy.
|
||||||
|
* @param configMap contains configuration needed by handler.
|
||||||
|
* @return permalink representing a unique attachment. Might be relative URL.
|
||||||
|
*/
|
||||||
|
default Mono<URI> getPermalink(Attachment attachment,
|
||||||
|
Policy policy,
|
||||||
|
ConfigMap configMap) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
interface UploadContext {
|
interface UploadContext {
|
||||||
|
|
||||||
FilePart file();
|
FilePart file();
|
||||||
|
@ -31,12 +71,4 @@ public interface AttachmentHandler extends ExtensionPoint {
|
||||||
ConfigMap configMap();
|
ConfigMap configMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
record UploadOption(FilePart file,
|
|
||||||
Policy policy,
|
|
||||||
ConfigMap configMap) implements UploadContext {
|
|
||||||
}
|
|
||||||
|
|
||||||
record DeleteOption(Attachment attachment, Policy policy, ConfigMap configMap)
|
|
||||||
implements DeleteContext {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package run.halo.app.core.extension.attachment.endpoint;
|
||||||
|
|
||||||
|
import run.halo.app.core.extension.attachment.Attachment;
|
||||||
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
|
||||||
|
public record DeleteOption(Attachment attachment, Policy policy, ConfigMap configMap)
|
||||||
|
implements AttachmentHandler.DeleteContext {
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package run.halo.app.core.extension.attachment.endpoint;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SimpleFilePart is an adapter of simple data for uploading.
|
||||||
|
*
|
||||||
|
* @param filename is name of the attachment file.
|
||||||
|
* @param content is binary data of the attachment file.
|
||||||
|
* @param mediaType is media type of the attachment file.
|
||||||
|
*/
|
||||||
|
record SimpleFilePart(
|
||||||
|
String filename,
|
||||||
|
Flux<DataBuffer> content,
|
||||||
|
MediaType mediaType
|
||||||
|
) implements FilePart {
|
||||||
|
@Override
|
||||||
|
public Mono<Void> transferTo(Path dest) {
|
||||||
|
return DataBufferUtils.write(content(), dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return filename();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpHeaders headers() {
|
||||||
|
var headers = new HttpHeaders();
|
||||||
|
headers.setContentType(mediaType);
|
||||||
|
return HttpHeaders.readOnlyHttpHeaders(headers);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package run.halo.app.core.extension.attachment.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
|
||||||
|
public record UploadOption(FilePart file,
|
||||||
|
Policy policy,
|
||||||
|
ConfigMap configMap) implements AttachmentHandler.UploadContext {
|
||||||
|
|
||||||
|
public static UploadOption from(String filename,
|
||||||
|
Flux<DataBuffer> content,
|
||||||
|
MediaType mediaType,
|
||||||
|
Policy policy,
|
||||||
|
ConfigMap configMap) {
|
||||||
|
var filePart = new SimpleFilePart(filename, content, mediaType);
|
||||||
|
return new UploadOption(filePart, policy, configMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package run.halo.app.core.extension.service;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.time.Duration;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.attachment.Attachment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AttachmentService
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @since 2.5.0
|
||||||
|
*/
|
||||||
|
public interface AttachmentService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the given attachment to specific storage using handlers in plugins. Please note
|
||||||
|
* that we will make sure the request is authenticated, or an unauthorized exception throws.
|
||||||
|
* <p>
|
||||||
|
* If no handler can be found to upload the given attachment, ServerError exception will be
|
||||||
|
* thrown.
|
||||||
|
*
|
||||||
|
* @param policyName is attachment policy name.
|
||||||
|
* @param groupName is group name the attachment belongs.
|
||||||
|
* @param filename is filename of the attachment.
|
||||||
|
* @param content is binary data of the attachment.
|
||||||
|
* @param mediaType is media type of the attachment.
|
||||||
|
* @return attachment.
|
||||||
|
*/
|
||||||
|
Mono<Attachment> upload(@NonNull String policyName,
|
||||||
|
@Nullable String groupName,
|
||||||
|
@NonNull String filename,
|
||||||
|
@NonNull Flux<DataBuffer> content,
|
||||||
|
@Nullable MediaType mediaType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an attachment using handlers in plugins.
|
||||||
|
* <p>
|
||||||
|
* If no handler can be found to delete the given attachment, Mono.empty() will return.
|
||||||
|
*
|
||||||
|
* @param attachment is to be deleted.
|
||||||
|
* @return deleted attachment.
|
||||||
|
*/
|
||||||
|
Mono<Attachment> delete(Attachment attachment);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets permalink using handlers in plugins.
|
||||||
|
* <p>
|
||||||
|
* If no handler can be found to delete the given attachment, Mono.empty() will return.
|
||||||
|
*
|
||||||
|
* @param attachment is created attachment.
|
||||||
|
* @return permalink
|
||||||
|
*/
|
||||||
|
Mono<URI> getPermalink(Attachment attachment);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets shared URL using handlers in plugins.
|
||||||
|
* <p>
|
||||||
|
* If no handler can be found to delete the given attachment, Mono.empty() will return.
|
||||||
|
*
|
||||||
|
* @param attachment is created attachment.
|
||||||
|
* @param ttl is time to live of the shared URL.
|
||||||
|
* @return time-to-live shared URL. Please note that, if the attachment is stored in local, the
|
||||||
|
* shared URL is equal to permalink.
|
||||||
|
*/
|
||||||
|
Mono<URI> getSharedURL(Attachment attachment, Duration ttl);
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder
|
||||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
||||||
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
|
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
|
||||||
import static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance;
|
import static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance;
|
||||||
import static org.springframework.web.reactive.function.BodyExtractors.toMultipartData;
|
|
||||||
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
|
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
|
||||||
import static run.halo.app.extension.ListResult.generateGenericClass;
|
import static run.halo.app.extension.ListResult.generateGenericClass;
|
||||||
import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersFromType;
|
import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersFromType;
|
||||||
|
@ -23,56 +22,61 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springdoc.core.fn.builders.requestbody.Builder;
|
import org.springdoc.core.fn.builders.requestbody.Builder;
|
||||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.multipart.FilePart;
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
import org.springframework.http.codec.multipart.FormFieldPart;
|
import org.springframework.http.codec.multipart.FormFieldPart;
|
||||||
import org.springframework.http.codec.multipart.Part;
|
import org.springframework.http.codec.multipart.Part;
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.reactive.function.BodyExtractors;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.attachment.Attachment;
|
import run.halo.app.core.extension.attachment.Attachment;
|
||||||
import run.halo.app.core.extension.attachment.Policy;
|
|
||||||
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler.UploadOption;
|
|
||||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||||
import run.halo.app.core.extension.endpoint.SortResolver;
|
import run.halo.app.core.extension.endpoint.SortResolver;
|
||||||
|
import run.halo.app.core.extension.service.AttachmentService;
|
||||||
import run.halo.app.extension.Comparators;
|
import run.halo.app.extension.Comparators;
|
||||||
import run.halo.app.extension.ConfigMap;
|
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.router.IListRequest;
|
import run.halo.app.extension.router.IListRequest;
|
||||||
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
||||||
import run.halo.app.plugin.ExtensionComponentsFinder;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class AttachmentEndpoint implements CustomEndpoint {
|
public class AttachmentEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
|
private final AttachmentService attachmentService;
|
||||||
|
|
||||||
private final ReactiveExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
private final ExtensionComponentsFinder extensionComponentsFinder;
|
public AttachmentEndpoint(AttachmentService attachmentService,
|
||||||
|
ReactiveExtensionClient client) {
|
||||||
public AttachmentEndpoint(ReactiveExtensionClient client,
|
this.attachmentService = attachmentService;
|
||||||
ExtensionComponentsFinder extensionComponentsFinder) {
|
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.extensionComponentsFinder = extensionComponentsFinder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RouterFunction<ServerResponse> endpoint() {
|
public RouterFunction<ServerResponse> endpoint() {
|
||||||
var tag = "api.console.halo.run/v1alpha1/Attachment";
|
var tag = "api.console.halo.run/v1alpha1/Attachment";
|
||||||
return SpringdocRouteBuilder.route()
|
return SpringdocRouteBuilder.route()
|
||||||
.POST("/attachments/upload", contentType(MediaType.MULTIPART_FORM_DATA), this::upload,
|
.POST("/attachments/upload", contentType(MediaType.MULTIPART_FORM_DATA),
|
||||||
|
request -> request.body(BodyExtractors.toMultipartData())
|
||||||
|
.map(UploadRequest::new)
|
||||||
|
.flatMap(uploadReq -> {
|
||||||
|
var policyName = uploadReq.getPolicyName();
|
||||||
|
var groupName = uploadReq.getGroupName();
|
||||||
|
var filePart = uploadReq.getFile();
|
||||||
|
return attachmentService.upload(policyName,
|
||||||
|
groupName,
|
||||||
|
filePart.filename(),
|
||||||
|
filePart.content(),
|
||||||
|
filePart.headers().getContentType());
|
||||||
|
})
|
||||||
|
.flatMap(attachment -> ServerResponse.ok().bodyValue(attachment)),
|
||||||
builder -> builder
|
builder -> builder
|
||||||
.operationId("UploadAttachment")
|
.operationId("UploadAttachment")
|
||||||
.tag(tag)
|
.tag(tag)
|
||||||
|
@ -98,56 +102,6 @@ public class AttachmentEndpoint implements CustomEndpoint {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
Mono<ServerResponse> upload(ServerRequest request) {
|
|
||||||
return ReactiveSecurityContextHolder.getContext()
|
|
||||||
.switchIfEmpty(Mono.error(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED,
|
|
||||||
"Please login first and try it again")))
|
|
||||||
.map(SecurityContext::getAuthentication)
|
|
||||||
.map(Authentication::getName)
|
|
||||||
.flatMap(username -> request.body(toMultipartData())
|
|
||||||
.map(UploadRequest::new)
|
|
||||||
// prepare the upload option
|
|
||||||
.flatMap(uploadRequest -> client.get(Policy.class, uploadRequest.getPolicyName())
|
|
||||||
.filter(policy -> StringUtils.hasText(policy.getSpec().getConfigMapName()))
|
|
||||||
.switchIfEmpty(Mono.error(() -> new ServerWebInputException(
|
|
||||||
"Please configure the attachment policy before uploading")))
|
|
||||||
.flatMap(policy -> {
|
|
||||||
var configMapName = policy.getSpec().getConfigMapName();
|
|
||||||
return client.get(ConfigMap.class, configMapName)
|
|
||||||
.map(configMap -> new UploadOption(uploadRequest.getFile(), policy,
|
|
||||||
configMap));
|
|
||||||
})
|
|
||||||
// find the proper handler to handle the attachment
|
|
||||||
.flatMap(uploadOption -> Flux.fromIterable(
|
|
||||||
extensionComponentsFinder.getExtensions(AttachmentHandler.class))
|
|
||||||
.concatMap(uploadHandler -> uploadHandler.upload(uploadOption)
|
|
||||||
.doOnNext(attachment -> {
|
|
||||||
var spec = attachment.getSpec();
|
|
||||||
if (spec == null) {
|
|
||||||
spec = new Attachment.AttachmentSpec();
|
|
||||||
attachment.setSpec(spec);
|
|
||||||
}
|
|
||||||
spec.setOwnerName(username);
|
|
||||||
spec.setPolicyName(uploadOption.policy().getMetadata().getName());
|
|
||||||
var groupName = uploadRequest.getGroupName();
|
|
||||||
if (groupName != null) {
|
|
||||||
// validate the group name
|
|
||||||
spec.setGroupName(groupName);
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.next()
|
|
||||||
.switchIfEmpty(Mono.error(
|
|
||||||
() -> new ResponseStatusException(
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
"No suitable handler found for uploading the attachment"))))
|
|
||||||
)
|
|
||||||
// create the attachment
|
|
||||||
.flatMap(client::create)
|
|
||||||
.flatMap(attachment -> ServerResponse.ok()
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.bodyValue(attachment)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Mono<ServerResponse> search(ServerRequest request) {
|
Mono<ServerResponse> search(ServerRequest request) {
|
||||||
var searchRequest = new SearchRequest(request);
|
var searchRequest = new SearchRequest(request);
|
||||||
return client.list(Attachment.class,
|
return client.list(Attachment.class,
|
||||||
|
|
|
@ -5,10 +5,12 @@ import static run.halo.app.infra.utils.FileNameUtils.randomFileName;
|
||||||
import static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;
|
import static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -22,6 +24,7 @@ import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
import org.springframework.web.util.UriUtils;
|
||||||
import reactor.core.Exceptions;
|
import reactor.core.Exceptions;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
@ -31,7 +34,9 @@ import run.halo.app.core.extension.attachment.Attachment;
|
||||||
import run.halo.app.core.extension.attachment.Attachment.AttachmentSpec;
|
import run.halo.app.core.extension.attachment.Attachment.AttachmentSpec;
|
||||||
import run.halo.app.core.extension.attachment.Constant;
|
import run.halo.app.core.extension.attachment.Constant;
|
||||||
import run.halo.app.core.extension.attachment.Policy;
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.infra.ExternalUrlSupplier;
|
||||||
import run.halo.app.infra.exception.AttachmentAlreadyExistsException;
|
import run.halo.app.infra.exception.AttachmentAlreadyExistsException;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
@ -42,8 +47,12 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
|
||||||
|
|
||||||
private final HaloProperties haloProp;
|
private final HaloProperties haloProp;
|
||||||
|
|
||||||
public LocalAttachmentUploadHandler(HaloProperties haloProp) {
|
private final ExternalUrlSupplier externalUrl;
|
||||||
|
|
||||||
|
public LocalAttachmentUploadHandler(HaloProperties haloProp,
|
||||||
|
ExternalUrlSupplier externalUrl) {
|
||||||
this.haloProp = haloProp;
|
this.haloProp = haloProp;
|
||||||
|
this.externalUrl = externalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path getAttachmentsRoot() {
|
Path getAttachmentsRoot() {
|
||||||
|
@ -153,6 +162,35 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
|
||||||
.map(DeleteContext::attachment);
|
.map(DeleteContext::attachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<URI> getPermalink(Attachment attachment, Policy policy, ConfigMap configMap) {
|
||||||
|
if (!this.shouldHandle(policy)) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
var annotations = attachment.getMetadata().getAnnotations();
|
||||||
|
if (annotations == null
|
||||||
|
|| !annotations.containsKey(Constant.URI_ANNO_KEY)) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
var uriStr = annotations.get(Constant.URI_ANNO_KEY);
|
||||||
|
// the uriStr is encoded before.
|
||||||
|
uriStr = UriUtils.decode(uriStr, StandardCharsets.UTF_8);
|
||||||
|
var uri = UriComponentsBuilder.fromUri(externalUrl.get())
|
||||||
|
// The URI has been encoded before, so there is no need to encode it again.
|
||||||
|
.path(uriStr)
|
||||||
|
.build()
|
||||||
|
.toUri();
|
||||||
|
return Mono.just(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<URI> getSharedURL(Attachment attachment,
|
||||||
|
Policy policy,
|
||||||
|
ConfigMap configMap,
|
||||||
|
Duration ttl) {
|
||||||
|
return getPermalink(attachment, policy, configMap);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldHandle(Policy policy) {
|
private boolean shouldHandle(Policy policy) {
|
||||||
if (policy == null
|
if (policy == null
|
||||||
|| policy.getSpec() == null
|
|| policy.getSpec() == null
|
||||||
|
|
|
@ -1,28 +1,22 @@
|
||||||
package run.halo.app.core.extension.reconciler.attachment;
|
package run.halo.app.core.extension.reconciler.attachment;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.attachment.Attachment;
|
import run.halo.app.core.extension.attachment.Attachment;
|
||||||
import run.halo.app.core.extension.attachment.Attachment.AttachmentStatus;
|
import run.halo.app.core.extension.attachment.Attachment.AttachmentStatus;
|
||||||
import run.halo.app.core.extension.attachment.Constant;
|
import run.halo.app.core.extension.attachment.Constant;
|
||||||
import run.halo.app.core.extension.attachment.Policy;
|
import run.halo.app.core.extension.service.AttachmentService;
|
||||||
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;
|
|
||||||
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler.DeleteOption;
|
|
||||||
import run.halo.app.extension.ConfigMap;
|
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.controller.Controller;
|
import run.halo.app.extension.controller.Controller;
|
||||||
import run.halo.app.extension.controller.ControllerBuilder;
|
import run.halo.app.extension.controller.ControllerBuilder;
|
||||||
import run.halo.app.extension.controller.Reconciler;
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
import run.halo.app.extension.controller.Reconciler.Request;
|
import run.halo.app.extension.controller.Reconciler.Request;
|
||||||
import run.halo.app.infra.ExternalUrlSupplier;
|
import run.halo.app.infra.ExternalUrlSupplier;
|
||||||
import run.halo.app.infra.exception.NotFoundException;
|
|
||||||
import run.halo.app.plugin.ExtensionComponentsFinder;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
@ -30,16 +24,15 @@ public class AttachmentReconciler implements Reconciler<Request> {
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ExtensionClient client;
|
||||||
|
|
||||||
private final ExtensionComponentsFinder extensionComponentsFinder;
|
|
||||||
|
|
||||||
private final ExternalUrlSupplier externalUrl;
|
private final ExternalUrlSupplier externalUrl;
|
||||||
|
|
||||||
|
private final AttachmentService attachmentService;
|
||||||
|
|
||||||
public AttachmentReconciler(ExtensionClient client,
|
public AttachmentReconciler(ExtensionClient client,
|
||||||
ExtensionComponentsFinder extensionComponentsFinder,
|
ExternalUrlSupplier externalUrl, AttachmentService attachmentService) {
|
||||||
ExternalUrlSupplier externalUrl) {
|
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.extensionComponentsFinder = extensionComponentsFinder;
|
|
||||||
this.externalUrl = externalUrl;
|
this.externalUrl = externalUrl;
|
||||||
|
this.attachmentService = attachmentService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -47,38 +40,24 @@ public class AttachmentReconciler implements Reconciler<Request> {
|
||||||
client.fetch(Attachment.class, request.name()).ifPresent(attachment -> {
|
client.fetch(Attachment.class, request.name()).ifPresent(attachment -> {
|
||||||
// TODO Handle the finalizer
|
// TODO Handle the finalizer
|
||||||
if (attachment.getMetadata().getDeletionTimestamp() != null) {
|
if (attachment.getMetadata().getDeletionTimestamp() != null) {
|
||||||
Policy policy = client.fetch(Policy.class, attachment.getSpec().getPolicyName())
|
attachmentService.delete(attachment)
|
||||||
.orElseThrow();
|
.doOnNext(deletedAttachment -> {
|
||||||
var configMap = client.fetch(ConfigMap.class, policy.getSpec().getConfigMapName())
|
removeFinalizer(attachment.getMetadata().getName());
|
||||||
.orElseThrow();
|
})
|
||||||
var deleteOption = new DeleteOption(attachment, policy, configMap);
|
.blockOptional();
|
||||||
Flux.fromIterable(extensionComponentsFinder.getExtensions(AttachmentHandler.class))
|
|
||||||
.concatMap(handler -> handler.delete(deleteOption)).next().switchIfEmpty(
|
|
||||||
Mono.error(() -> new NotFoundException(
|
|
||||||
"No suitable handler found to delete the attachment")))
|
|
||||||
.doOnNext(deleted -> removeFinalizer(deleted.getMetadata().getName())).block();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// add finalizer
|
// add finalizer
|
||||||
addFinalizerIfNotSet(request.name(), attachment.getMetadata().getFinalizers());
|
addFinalizerIfNotSet(request.name(), attachment.getMetadata().getFinalizers());
|
||||||
|
|
||||||
var annotations = attachment.getMetadata().getAnnotations();
|
var annotations = attachment.getMetadata().getAnnotations();
|
||||||
if (annotations != null) {
|
if (annotations != null) {
|
||||||
String permalink = null;
|
attachmentService.getPermalink(attachment)
|
||||||
var uri = annotations.get(Constant.URI_ANNO_KEY);
|
.map(URI::toString)
|
||||||
if (uri != null) {
|
.switchIfEmpty(Mono.fromSupplier(() -> {
|
||||||
permalink = UriComponentsBuilder.fromUri(externalUrl.get())
|
// Only for back-compatibility
|
||||||
// The URI has been encoded before, so there is no need to encode it again.
|
return annotations.get(Constant.EXTERNAL_LINK_ANNO_KEY);
|
||||||
.path(uri)
|
}))
|
||||||
.build()
|
.doOnNext(permalink -> {
|
||||||
.toString();
|
|
||||||
} else {
|
|
||||||
var externalLink = annotations.get(Constant.EXTERNAL_LINK_ANNO_KEY);
|
|
||||||
if (externalLink != null) {
|
|
||||||
permalink = externalLink;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (permalink != null) {
|
|
||||||
log.debug("Set permalink {} for attachment {}", permalink, request.name());
|
log.debug("Set permalink {} for attachment {}", permalink, request.name());
|
||||||
var status = attachment.getStatus();
|
var status = attachment.getStatus();
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
|
@ -86,7 +65,8 @@ public class AttachmentReconciler implements Reconciler<Request> {
|
||||||
attachment.setStatus(status);
|
attachment.setStatus(status);
|
||||||
}
|
}
|
||||||
status.setPermalink(permalink);
|
status.setPermalink(permalink);
|
||||||
}
|
})
|
||||||
|
.blockOptional();
|
||||||
}
|
}
|
||||||
updateStatus(request.name(), attachment.getStatus());
|
updateStatus(request.name(), attachment.getStatus());
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
package run.halo.app.core.extension.service.impl;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.web.server.ServerErrorException;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.attachment.Attachment;
|
||||||
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
|
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;
|
||||||
|
import run.halo.app.core.extension.attachment.endpoint.DeleteOption;
|
||||||
|
import run.halo.app.core.extension.attachment.endpoint.UploadOption;
|
||||||
|
import run.halo.app.core.extension.service.AttachmentService;
|
||||||
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.plugin.ExtensionComponentsFinder;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class DefaultAttachmentService implements AttachmentService {
|
||||||
|
|
||||||
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
private final ExtensionComponentsFinder extensionComponentsFinder;
|
||||||
|
|
||||||
|
public DefaultAttachmentService(ReactiveExtensionClient client,
|
||||||
|
ExtensionComponentsFinder extensionComponentsFinder) {
|
||||||
|
this.client = client;
|
||||||
|
this.extensionComponentsFinder = extensionComponentsFinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Attachment> upload(@NonNull String policyName,
|
||||||
|
@Nullable String groupName,
|
||||||
|
@NonNull String filename,
|
||||||
|
@NonNull Flux<DataBuffer> content,
|
||||||
|
@Nullable MediaType mediaType) {
|
||||||
|
return authenticationConsumer(authentication -> client.get(Policy.class, policyName)
|
||||||
|
.flatMap(policy -> {
|
||||||
|
var configMapName = policy.getSpec().getConfigMapName();
|
||||||
|
if (!StringUtils.hasText(configMapName)) {
|
||||||
|
return Mono.error(new ServerWebInputException(
|
||||||
|
"ConfigMap name not found in Policy " + policyName));
|
||||||
|
}
|
||||||
|
return client.get(ConfigMap.class, configMapName)
|
||||||
|
.map(configMap -> UploadOption.from(filename,
|
||||||
|
content,
|
||||||
|
mediaType,
|
||||||
|
policy,
|
||||||
|
configMap));
|
||||||
|
})
|
||||||
|
.flatMap(uploadContext -> {
|
||||||
|
var handlers = extensionComponentsFinder.getExtensions(AttachmentHandler.class);
|
||||||
|
return Flux.fromIterable(handlers)
|
||||||
|
.concatMap(handler -> handler.upload(uploadContext))
|
||||||
|
.next();
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.error(() -> new ServerErrorException(
|
||||||
|
"No suitable handler found for uploading the attachment.", null)))
|
||||||
|
.doOnNext(attachment -> {
|
||||||
|
var spec = attachment.getSpec();
|
||||||
|
if (spec == null) {
|
||||||
|
spec = new Attachment.AttachmentSpec();
|
||||||
|
attachment.setSpec(spec);
|
||||||
|
}
|
||||||
|
spec.setOwnerName(authentication.getName());
|
||||||
|
if (StringUtils.hasText(groupName)) {
|
||||||
|
spec.setGroupName(groupName);
|
||||||
|
}
|
||||||
|
spec.setPolicyName(policyName);
|
||||||
|
})
|
||||||
|
.flatMap(client::create));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Attachment> delete(Attachment attachment) {
|
||||||
|
var spec = attachment.getSpec();
|
||||||
|
return client.get(Policy.class, spec.getPolicyName())
|
||||||
|
.flatMap(policy -> client.get(ConfigMap.class, policy.getSpec().getConfigMapName())
|
||||||
|
.map(configMap -> new DeleteOption(attachment, policy, configMap)))
|
||||||
|
.flatMap(deleteOption -> {
|
||||||
|
var handlers = extensionComponentsFinder.getExtensions(AttachmentHandler.class);
|
||||||
|
return Flux.fromIterable(handlers)
|
||||||
|
.concatMap(handler -> handler.delete(deleteOption))
|
||||||
|
.next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<URI> getPermalink(Attachment attachment) {
|
||||||
|
var handlers = extensionComponentsFinder.getExtensions(AttachmentHandler.class);
|
||||||
|
return client.get(Policy.class, attachment.getSpec().getPolicyName())
|
||||||
|
.flatMap(policy -> client.get(ConfigMap.class, policy.getSpec().getConfigMapName())
|
||||||
|
.flatMap(configMap -> Flux.fromIterable(handlers)
|
||||||
|
.concatMap(handler -> handler.getPermalink(attachment, policy, configMap))
|
||||||
|
.next()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<URI> getSharedURL(Attachment attachment, Duration ttl) {
|
||||||
|
var handlers = extensionComponentsFinder.getExtensions(AttachmentHandler.class);
|
||||||
|
return client.get(Policy.class, attachment.getSpec().getPolicyName())
|
||||||
|
.flatMap(policy -> client.get(ConfigMap.class, policy.getSpec().getConfigMapName())
|
||||||
|
.flatMap(configMap -> Flux.fromIterable(handlers)
|
||||||
|
.concatMap(handler -> handler.getSharedURL(attachment, policy, configMap, ttl))
|
||||||
|
.next()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> Mono<T> authenticationConsumer(Function<Authentication, Mono<T>> func) {
|
||||||
|
return ReactiveSecurityContextHolder.getContext()
|
||||||
|
.switchIfEmpty(Mono.error(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED,
|
||||||
|
"Authentication required.")))
|
||||||
|
.map(SecurityContext::getAuthentication)
|
||||||
|
.flatMap(func);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import run.halo.app.core.extension.service.AttachmentService;
|
||||||
import run.halo.app.extension.DefaultSchemeManager;
|
import run.halo.app.extension.DefaultSchemeManager;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
@ -67,6 +68,8 @@ public class SharedApplicationContextHolder {
|
||||||
rootApplicationContext.getBean(ExternalUrlSupplier.class));
|
rootApplicationContext.getBean(ExternalUrlSupplier.class));
|
||||||
beanFactory.registerSingleton("serverSecurityContextRepository",
|
beanFactory.registerSingleton("serverSecurityContextRepository",
|
||||||
rootApplicationContext.getBean(ServerSecurityContextRepository.class));
|
rootApplicationContext.getBean(ServerSecurityContextRepository.class));
|
||||||
|
beanFactory.registerSingleton("attachmentService",
|
||||||
|
rootApplicationContext.getBean(AttachmentService.class));
|
||||||
// TODO add more shared instance here
|
// TODO add more shared instance here
|
||||||
|
|
||||||
return sharedApplicationContext;
|
return sharedApplicationContext;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.same;
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
|
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
|
||||||
|
@ -13,11 +14,11 @@ import static org.springframework.security.test.web.reactive.server.SecurityMock
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
@ -33,6 +34,7 @@ import run.halo.app.core.extension.attachment.Attachment.AttachmentSpec;
|
||||||
import run.halo.app.core.extension.attachment.Policy;
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
import run.halo.app.core.extension.attachment.Policy.PolicySpec;
|
import run.halo.app.core.extension.attachment.Policy.PolicySpec;
|
||||||
import run.halo.app.core.extension.attachment.endpoint.AttachmentEndpoint.SearchRequest;
|
import run.halo.app.core.extension.attachment.endpoint.AttachmentEndpoint.SearchRequest;
|
||||||
|
import run.halo.app.core.extension.service.impl.DefaultAttachmentService;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
@ -48,13 +50,14 @@ class AttachmentEndpointTest {
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionComponentsFinder extensionComponentsFinder;
|
ExtensionComponentsFinder extensionComponentsFinder;
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
AttachmentEndpoint endpoint;
|
AttachmentEndpoint endpoint;
|
||||||
|
|
||||||
WebTestClient webClient;
|
WebTestClient webClient;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
|
var attachmentService = new DefaultAttachmentService(client, extensionComponentsFinder);
|
||||||
|
endpoint = new AttachmentEndpoint(attachmentService, client);
|
||||||
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
|
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
|
||||||
.apply(springSecurity())
|
.apply(springSecurity())
|
||||||
.build();
|
.build();
|
||||||
|
@ -65,12 +68,44 @@ class AttachmentEndpointTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResponseErrorIfNotLogin() {
|
void shouldResponseErrorIfNotLogin() {
|
||||||
|
var policySpec = new PolicySpec();
|
||||||
|
policySpec.setConfigMapName("fake-configmap");
|
||||||
|
var policyMetadata = new Metadata();
|
||||||
|
policyMetadata.setName("fake-policy");
|
||||||
|
var policy = new Policy();
|
||||||
|
policy.setSpec(policySpec);
|
||||||
|
policy.setMetadata(policyMetadata);
|
||||||
|
|
||||||
|
var cm = new ConfigMap();
|
||||||
|
var cmMetadata = new Metadata();
|
||||||
|
cmMetadata.setName("fake-configmap");
|
||||||
|
cm.setData(Map.of());
|
||||||
|
|
||||||
|
var handler = mock(AttachmentHandler.class);
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("fake-attachment");
|
||||||
|
var attachment = new Attachment();
|
||||||
|
attachment.setMetadata(metadata);
|
||||||
|
|
||||||
|
var builder = new MultipartBodyBuilder();
|
||||||
|
builder.part("policyName", "fake-policy");
|
||||||
|
builder.part("groupName", "fake-group");
|
||||||
|
builder.part("file", "fake-file")
|
||||||
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.filename("fake-filename");
|
||||||
webClient
|
webClient
|
||||||
.post()
|
.post()
|
||||||
.uri("/attachments/upload")
|
.uri("/attachments/upload")
|
||||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||||
|
.body(BodyInserters.fromMultipartData(builder.build()))
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isUnauthorized();
|
.expectStatus().isUnauthorized();
|
||||||
|
|
||||||
|
verify(client, never()).get(Policy.class, "fake-policy");
|
||||||
|
verify(client, never()).get(ConfigMap.class, "fake-configmap");
|
||||||
|
verify(client, never()).create(attachment);
|
||||||
|
verify(extensionComponentsFinder, never()).getExtensions(AttachmentHandler.class);
|
||||||
|
verify(handler, never()).upload(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -100,6 +135,44 @@ class AttachmentEndpointTest {
|
||||||
.expectStatus().isBadRequest();
|
.expectStatus().isBadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void prepareForUploading(Consumer<MultipartBodyBuilder> consumer) {
|
||||||
|
var policySpec = new PolicySpec();
|
||||||
|
policySpec.setConfigMapName("fake-configmap");
|
||||||
|
var policyMetadata = new Metadata();
|
||||||
|
policyMetadata.setName("fake-policy");
|
||||||
|
var policy = new Policy();
|
||||||
|
policy.setSpec(policySpec);
|
||||||
|
policy.setMetadata(policyMetadata);
|
||||||
|
|
||||||
|
var cm = new ConfigMap();
|
||||||
|
var cmMetadata = new Metadata();
|
||||||
|
cmMetadata.setName("fake-configmap");
|
||||||
|
cm.setData(Map.of());
|
||||||
|
|
||||||
|
when(client.get(Policy.class, "fake-policy")).thenReturn(Mono.just(policy));
|
||||||
|
when(client.get(ConfigMap.class, "fake-configmap")).thenReturn(Mono.just(cm));
|
||||||
|
|
||||||
|
var handler = mock(AttachmentHandler.class);
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("fake-attachment");
|
||||||
|
var attachment = new Attachment();
|
||||||
|
attachment.setMetadata(metadata);
|
||||||
|
|
||||||
|
when(handler.upload(any())).thenReturn(Mono.just(attachment));
|
||||||
|
when(extensionComponentsFinder.getExtensions(AttachmentHandler.class)).thenReturn(
|
||||||
|
List.of(handler));
|
||||||
|
when(client.create(attachment)).thenReturn(Mono.just(attachment));
|
||||||
|
|
||||||
|
var builder = new MultipartBodyBuilder();
|
||||||
|
builder.part("policyName", "fake-policy");
|
||||||
|
builder.part("groupName", "fake-group");
|
||||||
|
builder.part("file", "fake-file")
|
||||||
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.filename("fake-filename");
|
||||||
|
|
||||||
|
consumer.accept(builder);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUploadSuccessfully() {
|
void shouldUploadSuccessfully() {
|
||||||
var policySpec = new PolicySpec();
|
var policySpec = new PolicySpec();
|
||||||
|
|
Loading…
Reference in New Issue