Skip to content

Commit

Permalink
feat: allow users to manage their attachments in uc
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Sep 29, 2024
1 parent 1947a54 commit 4930125
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 145 deletions.
1 change: 1 addition & 0 deletions api/src/main/java/run/halo/app/infra/SystemSetting.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public static class User {
boolean mustVerifyEmailOnRegistration;
String defaultRole;
String avatarPolicy;
String ucAttachmentPolicy;
}

@Data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package run.halo.app.core.attachment;

import reactor.core.publisher.Mono;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.extension.ListResult;

public interface AttachmentLister {

Mono<ListResult<Attachment>> listBy(SearchRequest searchRequest);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package run.halo.app.core.attachment;

import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
import static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.in;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import static run.halo.app.extension.index.query.QueryFactory.not;
import static run.halo.app.extension.index.query.QueryFactory.startsWith;

import io.swagger.v3.oas.annotations.enums.ParameterIn;
import java.util.List;
import java.util.Optional;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.ServerRequest;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.QueryParamBuildUtil;
import run.halo.app.extension.router.SortableRequest;

public class SearchRequest extends SortableRequest {

public SearchRequest(ServerRequest request) {
super(request.exchange());
}

public Optional<String> getKeyword() {
return Optional.ofNullable(queryParams.getFirst("keyword"))
.filter(StringUtils::hasText);
}

public Optional<Boolean> getUngrouped() {
return Optional.ofNullable(queryParams.getFirst("ungrouped"))
.map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class));
}

public Optional<List<String>> getAccepts() {
return Optional.ofNullable(queryParams.get("accepts"))
.filter(accepts -> !accepts.isEmpty()
&& !accepts.contains("*")
&& !accepts.contains("*/*")
);
}

public ListOptions toListOptions(List<String> hiddenGroups) {
var builder = ListOptions.builder(super.toListOptions());

getKeyword().ifPresent(keyword -> {
builder.andQuery(contains("spec.displayName", keyword));
});

getUngrouped()
.filter(ungrouped -> ungrouped)
.ifPresent(ungrouped -> builder.andQuery(isNull("spec.groupName")));

if (!CollectionUtils.isEmpty(hiddenGroups)) {
builder.andQuery(not(in("spec.groupName", hiddenGroups)));
}

getAccepts().flatMap(accepts -> accepts.stream()
.filter(StringUtils::hasText)
.map(accept -> accept.replace("/*", "/").toLowerCase())
.distinct()
.map(accept -> startsWith("spec.mediaType", accept))
.reduce(QueryFactory::or)
)
.ifPresent(builder::andQuery);

return builder.build();
}

public static void buildParameters(Builder builder) {
IListRequest.buildParameters(builder);
builder.parameter(QueryParamBuildUtil.sortParameter())
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("ungrouped")
.required(false)
.description("""
Filter attachments without group. This parameter will ignore group \
parameter.\
""")
.implementation(Boolean.class))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("keyword")
.required(false)
.description("Keyword for searching.")
.implementation(String.class))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("accepts")
.required(false)
.description("Acceptable media types.")
.array(
arraySchemaBuilder()
.uniqueItems(true)
.schema(schemaBuilder()
.implementation(String.class)
.example("image/*"))
)
.implementationArray(String.class)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,23 @@

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder;
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
import static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static run.halo.app.extension.ListResult.generateGenericClass;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.in;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import static run.halo.app.extension.index.query.QueryFactory.not;
import static run.halo.app.extension.index.query.QueryFactory.startsWith;

import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import java.net.URL;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.codec.multipart.FormFieldPart;
import org.springframework.http.codec.multipart.Part;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyExtractors;
Expand All @@ -41,18 +27,11 @@
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import run.halo.app.core.attachment.AttachmentLister;
import run.halo.app.core.attachment.SearchRequest;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.core.extension.attachment.Group;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.core.extension.service.AttachmentService;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.QueryParamBuildUtil;
import run.halo.app.extension.router.SortableRequest;
import run.halo.app.extension.router.selector.LabelSelector;

@Slf4j
@Component
Expand All @@ -61,7 +40,7 @@ public class AttachmentEndpoint implements CustomEndpoint {

private final AttachmentService attachmentService;

private final ReactiveExtensionClient client;
private final AttachmentLister attachmentLister;

@Override
public RouterFunction<ServerResponse> endpoint() {
Expand Down Expand Up @@ -131,112 +110,13 @@ public RouterFunction<ServerResponse> endpoint() {

Mono<ServerResponse> search(ServerRequest request) {
var searchRequest = new SearchRequest(request);
var groupListOptions = new ListOptions();
groupListOptions.setLabelSelector(LabelSelector.builder()
.exists(Group.HIDDEN_LABEL)
.build());
return client.listAll(Group.class, groupListOptions, Sort.unsorted())
.map(group -> group.getMetadata().getName())
.collectList()
.defaultIfEmpty(List.of())
.flatMap(hiddenGroups -> client.listBy(Attachment.class,
searchRequest.toListOptions(hiddenGroups),
PageRequestImpl.of(searchRequest.getPage(), searchRequest.getSize(),
searchRequest.getSort())
)
.flatMap(listResult -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(listResult)
)
return attachmentLister.listBy(searchRequest)
.flatMap(listResult -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(listResult)
);
}

public static class SearchRequest extends SortableRequest {

public SearchRequest(ServerRequest request) {
super(request.exchange());
}

public Optional<String> getKeyword() {
return Optional.ofNullable(queryParams.getFirst("keyword"))
.filter(StringUtils::hasText);
}

public Optional<Boolean> getUngrouped() {
return Optional.ofNullable(queryParams.getFirst("ungrouped"))
.map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class));
}

public Optional<List<String>> getAccepts() {
return Optional.ofNullable(queryParams.get("accepts"))
.filter(accepts -> !accepts.isEmpty()
&& !accepts.contains("*")
&& !accepts.contains("*/*")
);
}

public ListOptions toListOptions(List<String> hiddenGroups) {
var builder = ListOptions.builder(super.toListOptions());

getKeyword().ifPresent(keyword -> {
builder.andQuery(contains("spec.displayName", keyword));
});

getUngrouped()
.filter(ungrouped -> ungrouped)
.ifPresent(ungrouped -> builder.andQuery(isNull("spec.groupName")));

if (!CollectionUtils.isEmpty(hiddenGroups)) {
builder.andQuery(not(in("spec.groupName", hiddenGroups)));
}

getAccepts().flatMap(accepts -> accepts.stream()
.filter(StringUtils::hasText)
.map(accept -> accept.replace("/*", "/").toLowerCase())
.distinct()
.map(accept -> startsWith("spec.mediaType", accept))
.reduce(QueryFactory::or)
)
.ifPresent(builder::andQuery);

return builder.build();
}

public static void buildParameters(Builder builder) {
IListRequest.buildParameters(builder);
builder.parameter(QueryParamBuildUtil.sortParameter())
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("ungrouped")
.required(false)
.description("""
Filter attachments without group. This parameter will ignore group \
parameter.\
""")
.implementation(Boolean.class))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("keyword")
.required(false)
.description("Keyword for searching.")
.implementation(String.class))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("accepts")
.required(false)
.description("Acceptable media types.")
.array(
arraySchemaBuilder()
.uniqueItems(true)
.schema(schemaBuilder()
.implementation(String.class)
.example("image/*"))
)
.implementationArray(String.class)
);
}
}

public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,
@Schema(requiredMode = REQUIRED) String policyName,
String groupName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package run.halo.app.core.attachment.impl;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.core.attachment.AttachmentLister;
import run.halo.app.core.attachment.SearchRequest;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.core.extension.attachment.Group;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.ReactiveExtensionClient;

@Component
@RequiredArgsConstructor
public class AttachmentListerImpl implements AttachmentLister {
private final ReactiveExtensionClient client;

@Override
public Mono<ListResult<Attachment>> listBy(SearchRequest searchRequest) {
var groupListOptions = ListOptions.builder()
.labelSelector()
.exists(Group.HIDDEN_LABEL)
.end()
.build();
return client.listAll(Group.class, groupListOptions, Sort.unsorted())
.map(group -> group.getMetadata().getName())
.collectList()
.defaultIfEmpty(List.of())
.flatMap(hiddenGroups -> client.listBy(Attachment.class,
searchRequest.toListOptions(hiddenGroups),
searchRequest.toPageRequest()
));
}
}
Loading

0 comments on commit 4930125

Please sign in to comment.