feat: API to save external links as attachments (#6364)

#### What type of PR is this?

/kind api-change
/kind feature
/area core

#### What this PR does / why we need it:

see #2335 

增加将第三方资源转存为附件资源的接口。

`/apis/api.console.halo.run/v1alpha1/attachments/-/upload-from-url`

UC:

`/apis/uc.api.content.halo.run/v1alpha1/attachments/-/upload-from-url`

其中参数为

```json
{
  "url": "string",
  "filename": "string",
  "groupName": "string",
  "policyName": "string"
}
```

#### How to test it?

测试能否将第三方接口的资源保存至附件中。
测试各类附件,例如图片、视频、文本等。

#### Does this PR introduce a user-facing change?
```release-note
增加通过链接转存第三方资源至附件库的接口
```
pull/6515/head
Takagi 2024-08-26 14:31:14 +08:00 committed by GitHub
parent 21db06a507
commit e5bbbb3b7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 749 additions and 33 deletions

View File

@ -2250,6 +2250,36 @@
]
}
},
"/apis/api.console.halo.run/v1alpha1/attachments/-/upload-from-url": {
"post": {
"operationId": "ExternalTransferAttachment",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UploadFromUrlRequest"
}
}
},
"required": true
},
"responses": {
"default": {
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Attachment"
}
}
},
"description": "default response"
}
},
"tags": [
"AttachmentV1alpha1Console"
]
}
},
"/apis/api.console.halo.run/v1alpha1/attachments/upload": {
"post": {
"operationId": "UploadAttachment",
@ -14136,6 +14166,47 @@
]
}
},
"/apis/uc.api.content.halo.run/v1alpha1/attachments/-/upload-from-url": {
"post": {
"description": "Upload attachment from the given URL.",
"operationId": "ExternalTransferAttachment_1",
"parameters": [
{
"description": "Wait for permalink.",
"in": "query",
"name": "waitForPermalink",
"schema": {
"type": "boolean"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UploadFromUrlRequest"
}
}
},
"required": true
},
"responses": {
"default": {
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Attachment"
}
}
},
"description": "default response"
}
},
"tags": [
"AttachmentV1alpha1Uc"
]
}
},
"/apis/uc.api.content.halo.run/v1alpha1/posts": {
"get": {
"description": "List posts owned by the current user.",
@ -22621,6 +22692,21 @@
}
}
},
"UploadFromUrlRequest": {
"required": [
"url"
],
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"url": {
"type": "string",
"format": "url"
}
}
},
"User": {
"required": [
"apiVersion",

View File

@ -117,6 +117,36 @@
]
}
},
"/apis/api.console.halo.run/v1alpha1/attachments/-/upload-from-url": {
"post": {
"operationId": "ExternalTransferAttachment",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UploadFromUrlRequest"
}
}
},
"required": true
},
"responses": {
"default": {
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Attachment"
}
}
},
"description": "default response"
}
},
"tags": [
"AttachmentV1alpha1Console"
]
}
},
"/apis/api.console.halo.run/v1alpha1/attachments/upload": {
"post": {
"operationId": "UploadAttachment",
@ -6246,6 +6276,28 @@
}
}
},
"UploadFromUrlRequest": {
"required": [
"policyName",
"url"
],
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"groupName": {
"type": "string"
},
"policyName": {
"type": "string"
},
"url": {
"type": "string",
"format": "url"
}
}
},
"User": {
"required": [
"apiVersion",

View File

@ -57,6 +57,47 @@
]
}
},
"/apis/uc.api.content.halo.run/v1alpha1/attachments/-/upload-from-url": {
"post": {
"description": "Upload attachment from the given URL.",
"operationId": "ExternalTransferAttachment",
"parameters": [
{
"description": "Wait for permalink.",
"in": "query",
"name": "waitForPermalink",
"schema": {
"type": "boolean"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UploadFromUrlRequest"
}
}
},
"required": true
},
"responses": {
"default": {
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Attachment"
}
}
},
"description": "default response"
}
},
"tags": [
"AttachmentV1alpha1Uc"
]
}
},
"/apis/uc.api.content.halo.run/v1alpha1/posts": {
"get": {
"description": "List posts owned by the current user.",
@ -1943,6 +1984,21 @@
}
}
},
"UploadFromUrlRequest": {
"required": [
"url"
],
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"url": {
"type": "string",
"format": "url"
}
}
},
"UserDevice": {
"required": [
"active",

View File

@ -1,6 +1,7 @@
package run.halo.app.core.extension.service;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
import java.util.function.Consumer;
import org.springframework.core.io.buffer.DataBuffer;
@ -91,4 +92,15 @@ public interface AttachmentService {
*/
Mono<URI> getSharedURL(Attachment attachment, Duration ttl);
/**
* Transfer external links to attachments.
*
* @param url external url
* @param policyName policy name
* @param groupName group name
* @param filename filename
* @return attachment
*/
Mono<Attachment> uploadFromUrl(@NonNull URL url, @NonNull String policyName,
String groupName, String filename);
}

View File

@ -22,8 +22,10 @@ import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldS
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
@ -102,6 +104,29 @@ public class AttachmentEndpoint implements CustomEndpoint {
))
.response(responseBuilder().implementation(Attachment.class))
.build())
.POST("/attachments/-/upload-from-url", contentType(MediaType.APPLICATION_JSON),
request -> request.bodyToMono(UploadFromUrlRequest.class)
.flatMap(uploadFromUrlRequest -> {
var url = uploadFromUrlRequest.url();
var policyName = uploadFromUrlRequest.policyName();
var groupName = uploadFromUrlRequest.groupName();
var fileName = uploadFromUrlRequest.filename();
return attachmentService.uploadFromUrl(url, policyName,
groupName, fileName);
})
.flatMap(attachment -> ServerResponse.ok().bodyValue(attachment)),
builder -> builder
.operationId("ExternalTransferAttachment")
.tag(tag)
.requestBody(requestBodyBuilder()
.required(true)
.content(contentBuilder()
.mediaType(MediaType.APPLICATION_JSON_VALUE)
.schema(schemaBuilder().implementation(UploadFromUrlRequest.class))
))
.response(responseBuilder().implementation(Attachment.class))
.build()
)
.GET("/attachments", this::search,
builder -> {
builder
@ -275,6 +300,21 @@ public class AttachmentEndpoint implements CustomEndpoint {
}
}
public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,
@Schema(requiredMode = REQUIRED) String policyName,
String groupName,
String filename) {
public UploadFromUrlRequest {
if (Objects.isNull(url)) {
throw new ServerWebInputException("Required url is missing.");
}
if (!StringUtils.hasText(policyName)) {
throw new ServerWebInputException("Policy name must not be blank");
}
}
}
@Schema(types = "object")
public interface IUploadRequest {

View File

@ -1,10 +1,14 @@
package run.halo.app.core.extension.service.impl;
import java.net.URI;
import java.net.URL;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
@ -29,6 +33,7 @@ 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.infra.ReactiveUrlDataBufferFetcher;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
@Component
@ -38,10 +43,14 @@ public class DefaultAttachmentService implements AttachmentService {
private final ExtensionGetter extensionGetter;
private final ReactiveUrlDataBufferFetcher dataBufferFetcher;
public DefaultAttachmentService(ReactiveExtensionClient client,
ExtensionGetter extensionGetter) {
ExtensionGetter extensionGetter,
ReactiveUrlDataBufferFetcher dataBufferFetcher) {
this.client = client;
this.extensionGetter = extensionGetter;
this.dataBufferFetcher = dataBufferFetcher;
}
@Override
@ -130,6 +139,45 @@ public class DefaultAttachmentService implements AttachmentService {
);
}
@Override
public Mono<Attachment> uploadFromUrl(@NonNull URL url, @NonNull String policyName,
String groupName, String filename) {
var uri = URI.create(url.toString());
AtomicReference<MediaType> mediaTypeRef = new AtomicReference<>();
AtomicReference<String> fileNameRef = new AtomicReference<>(filename);
Mono<Flux<DataBuffer>> contentMono = dataBufferFetcher.head(uri)
.map(response -> {
var httpHeaders = response.getHeaders();
if (!StringUtils.hasText(fileNameRef.get())) {
fileNameRef.set(getExternalUrlFilename(uri, httpHeaders));
}
MediaType contentType = httpHeaders.getContentType();
mediaTypeRef.set(contentType);
return response;
})
.map(response -> dataBufferFetcher.fetch(uri));
return contentMono.flatMap(
(content) -> upload(policyName, groupName, fileNameRef.get(), content,
mediaTypeRef.get())
)
.onErrorResume(throwable -> Mono.error(
new ServerWebInputException(
"Failed to transfer the attachment from the external URL."))
);
}
private static String getExternalUrlFilename(URI externalUrl, HttpHeaders httpHeaders) {
String fileName = httpHeaders.getContentDisposition().getFilename();
if (!StringUtils.hasText(fileName)) {
var path = externalUrl.getPath();
fileName = Paths.get(path).getFileName().toString();
}
// TODO get file extension from media type
return fileName;
}
private <T> Mono<T> authenticationConsumer(Function<Authentication, Mono<T>> func) {
return ReactiveSecurityContextHolder.getContext()
.switchIfEmpty(Mono.error(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED,

View File

@ -8,10 +8,13 @@ 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.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import java.net.URL;
import java.util.HashMap;
import java.util.Objects;
import java.util.function.Consumer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
@ -79,53 +82,106 @@ public class UcPostAttachmentEndpoint implements CustomEndpoint {
)
.response(responseBuilder().implementation(Attachment.class))
)
.POST("/attachments/-/upload-from-url", contentType(MediaType.APPLICATION_JSON),
this::uploadFromUrlForPost,
builder -> builder
.operationId("ExternalTransferAttachment")
.description("Upload attachment from the given URL.")
.tag(tag)
.parameter(parameterBuilder()
.name("waitForPermalink")
.description("Wait for permalink.")
.in(ParameterIn.QUERY)
.required(false)
.implementation(boolean.class))
.requestBody(requestBodyBuilder()
.required(true)
.content(contentBuilder()
.mediaType(MediaType.APPLICATION_JSON_VALUE)
.schema(schemaBuilder().implementation(UploadFromUrlRequest.class))
))
.response(responseBuilder().implementation(Attachment.class))
.build()
)
.build();
}
private Mono<ServerResponse> uploadFromUrlForPost(ServerRequest request) {
var uploadFromUrlRequestMono = request.bodyToMono(UploadFromUrlRequest.class);
var uploadAttachment = getPostSettingMono()
.flatMap(postSetting -> uploadFromUrlRequestMono.flatMap(
uploadFromUrlRequest -> {
var url = uploadFromUrlRequest.url();
var fileName = uploadFromUrlRequest.filename();
return attachmentService.uploadFromUrl(url,
postSetting.getAttachmentPolicyName(),
postSetting.getAttachmentGroupName(),
fileName
);
})
);
var waitForPermalink = request.queryParam("waitForPermalink")
.map(Boolean::valueOf)
.orElse(false);
if (waitForPermalink) {
uploadAttachment = waitForPermalink(uploadAttachment);
}
return ServerResponse.ok().body(uploadAttachment, Attachment.class);
}
private Mono<ServerResponse> createAttachmentForPost(ServerRequest request) {
var postAttachmentRequestMono = request.body(BodyExtractors.toMultipartData())
.map(PostAttachmentRequest::from)
.cache();
var postSettingMono = systemSettingFetcher.fetchPost()
.<SystemSetting.Post>handle((postSetting, sink) -> {
var attachmentPolicyName = postSetting.getAttachmentPolicyName();
if (StringUtils.isBlank(attachmentPolicyName)) {
sink.error(new ServerWebInputException(
"Please configure storage policy for post attachment first."));
return;
}
sink.next(postSetting);
});
// get settings
var createdAttachment = postSettingMono.flatMap(postSetting -> postAttachmentRequestMono
.flatMap(postAttachmentRequest -> getCurrentUser().flatMap(
username -> attachmentService.upload(username,
postSetting.getAttachmentPolicyName(),
postSetting.getAttachmentGroupName(),
postAttachmentRequest.file(),
linkWith(postAttachmentRequest)))));
var createdAttachment =
getPostSettingMono().flatMap(postSetting -> postAttachmentRequestMono
.flatMap(postAttachmentRequest -> getCurrentUser().flatMap(
username -> attachmentService.upload(username,
postSetting.getAttachmentPolicyName(),
postSetting.getAttachmentGroupName(),
postAttachmentRequest.file(),
linkWith(postAttachmentRequest)))));
var waitForPermalink = request.queryParam("waitForPermalink")
.map(Boolean::valueOf)
.orElse(false);
if (waitForPermalink) {
createdAttachment = createdAttachment.flatMap(attachment ->
attachmentService.getPermalink(attachment)
.doOnNext(permalink -> {
var status = attachment.getStatus();
if (status == null) {
status = new Attachment.AttachmentStatus();
attachment.setStatus(status);
}
status.setPermalink(permalink.toString());
})
.thenReturn(attachment));
createdAttachment = waitForPermalink(createdAttachment);
}
return ServerResponse.ok().body(createdAttachment, Attachment.class);
}
private Mono<Attachment> waitForPermalink(Mono<Attachment> createdAttachment) {
createdAttachment = createdAttachment.flatMap(attachment ->
attachmentService.getPermalink(attachment)
.doOnNext(permalink -> {
var status = attachment.getStatus();
if (status == null) {
status = new Attachment.AttachmentStatus();
attachment.setStatus(status);
}
status.setPermalink(permalink.toString());
})
.thenReturn(attachment));
return createdAttachment;
}
private Mono<SystemSetting.Post> getPostSettingMono() {
return systemSettingFetcher.fetchPost().handle((postSetting, sink) -> {
var attachmentPolicyName = postSetting.getAttachmentPolicyName();
if (StringUtils.isBlank(attachmentPolicyName)) {
sink.error(new ServerWebInputException(
"Please configure storage policy for post attachment first."));
return;
}
sink.next(postSetting);
});
}
private Consumer<Attachment> linkWith(PostAttachmentRequest request) {
return attachment -> {
var labels = attachment.getMetadata().getLabels();
@ -165,6 +221,15 @@ public class UcPostAttachmentEndpoint implements CustomEndpoint {
return GroupVersion.parseAPIVersion("uc.api.content.halo.run/v1alpha1");
}
public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,
String filename) {
public UploadFromUrlRequest {
if (Objects.isNull(url)) {
throw new ServerWebInputException("Required url is missing.");
}
}
}
@Schema(types = "object")
public record PostAttachmentRequest(
@Schema(requiredMode = REQUIRED, description = "Attachment data.")

View File

@ -3,10 +3,12 @@ package run.halo.app.infra;
import java.net.URI;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
/**
@ -31,4 +33,12 @@ public class DefaultReactiveUrlDataBufferFetcher implements ReactiveUrlDataBuffe
.retrieve()
.bodyToFlux(DataBuffer.class);
}
@Override
public Mono<ResponseEntity<Void>> head(URI uri) {
return webClient.head()
.uri(uri)
.retrieve()
.toBodilessEntity();
}
}

View File

@ -2,7 +2,9 @@ package run.halo.app.infra;
import java.net.URI;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.ResponseEntity;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* <p>{@link DataBuffer} stream fetcher from uri.</p>
@ -10,7 +12,6 @@ import reactor.core.publisher.Flux;
* @author guqing
* @since 2.6.0
*/
@FunctionalInterface
public interface ReactiveUrlDataBufferFetcher {
/**
@ -20,4 +21,12 @@ public interface ReactiveUrlDataBufferFetcher {
* @return data buffer flux
*/
Flux<DataBuffer> fetch(URI uri);
/**
* <p>Get head of the uri.</p>
*
* @param uri uri to fetch
* @return response entity
*/
Mono<ResponseEntity<Void>> head(URI uri);
}

View File

@ -1,6 +1,7 @@
package run.halo.app.infra.utils;
import com.google.common.io.Files;
import java.util.regex.Pattern;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
@ -9,6 +10,20 @@ public final class FileNameUtils {
private FileNameUtils() {
}
/**
* Check whether the file name has an extension.
*
* @param filename is name of file.
* @return True if file name has extension, otherwise false.
*/
public static boolean hasFileExtension(String filename) {
if (filename == null || filename.isEmpty()) {
return false;
}
var extensionRegex = ".*\\.[a-zA-Z0-9]+$";
return Pattern.matches(extensionRegex, filename);
}
public static String removeFileExtension(String filename, boolean removeAllExtensions) {
if (filename == null || filename.isEmpty()) {
return filename;

View File

@ -17,10 +17,15 @@ rules:
- apiGroups: [ "api.console.halo.run" ]
resources: [ "attachments" ]
verbs: [ "*" ]
- apiGroups: [ "api.console.halo.run" ]
resources: [ "attachments/upload-from-url" ]
verbs: [ "create" ]
- apiGroups: [ "" ]
resources: [ "settings" ]
verbs: [ "get" ]
- nonResourceURLs: [ "/apis/api.console.halo.run/v1alpha1/attachments/upload" ]
- nonResourceURLs: [
"/apis/api.console.halo.run/v1alpha1/attachments/upload"
]
verbs: [ "create" ]
---
apiVersion: v1alpha1

View File

@ -114,3 +114,6 @@ rules:
- apiGroups: [ "uc.api.content.halo.run" ]
resources: [ "attachments" ]
verbs: [ "create", "update", "delete" ]
- apiGroups: [ "uc.api.content.halo.run" ]
resources: [ "attachments/upload-from-url" ]
verbs: [ "create" ]

View File

@ -18,8 +18,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
@ -35,6 +38,7 @@ import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
@ExtendWith(MockitoExtension.class)
@ -46,13 +50,17 @@ class AttachmentEndpointTest {
@Mock
ExtensionGetter extensionGetter;
@Mock
ReactiveUrlDataBufferFetcher dataBufferFetcher;
AttachmentEndpoint endpoint;
WebTestClient webClient;
@BeforeEach
void setUp() {
var attachmentService = new DefaultAttachmentService(client, extensionGetter);
var attachmentService =
new DefaultAttachmentService(client, extensionGetter, dataBufferFetcher);
endpoint = new AttachmentEndpoint(attachmentService, client);
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
.apply(springSecurity())
@ -263,4 +271,78 @@ class AttachmentEndpointTest {
verify(client).listBy(eq(Attachment.class), any(), any(PageRequest.class));
}
}
@Nested
class ExternalTransferTest {
@Test
void shouldResponseErrorIfNoPermalinkProvided() {
webClient
.mutateWith(mockUser("fake-user").password("fake-password"))
.post()
.uri("/attachments/-/upload-from-url")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(Map.of("policyName", "fake-policy"))
.exchange()
.expectStatus().isBadRequest();
}
@Test
void shouldTransferSuccessfully() {
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);
ResponseEntity<Void> response = new ResponseEntity<>(HttpStatusCode.valueOf(200));
DataBuffer dataBuffer = mock(DataBuffer.class);
when(handler.upload(any())).thenReturn(Mono.just(attachment));
when(dataBufferFetcher.head(any())).thenReturn(Mono.just(response));
when(dataBufferFetcher.fetch(any())).thenReturn(Flux.just(dataBuffer));
when(extensionGetter.getExtensions(AttachmentHandler.class))
.thenReturn(Flux.just(handler));
when(client.create(attachment)).thenReturn(Mono.just(attachment));
var fakeValue =
Map.of("policyName", "fake-policy", "url",
"http://localhost:8090/fake-url.jpg");
webClient
.mutateWith(mockUser("fake-user").password("fake-password"))
.post()
.uri("/attachments/-/upload-from-url")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(fakeValue)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.metadata.name").isEqualTo("fake-attachment")
.jsonPath("$.spec.ownerName").isEqualTo("fake-user")
.jsonPath("$.spec.policyName").isEqualTo("fake-policy")
;
verify(client).get(Policy.class, "fake-policy");
verify(client).get(ConfigMap.class, "fake-configmap");
verify(client).create(attachment);
verify(dataBufferFetcher).head(any());
verify(dataBufferFetcher).fetch(any());
verify(handler).upload(any());
}
}
}

View File

@ -320,6 +320,7 @@ models/totp-auth-link-response.ts
models/totp-request.ts
models/two-factor-auth-settings.ts
models/upgrade-from-uri-request.ts
models/upload-from-url-request.ts
models/user-connection-list.ts
models/user-connection-spec.ts
models/user-connection.ts

View File

@ -25,12 +25,57 @@ import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError, ope
import { Attachment } from '../models';
// @ts-ignore
import { AttachmentList } from '../models';
// @ts-ignore
import { UploadFromUrlRequest } from '../models';
/**
* AttachmentV1alpha1ConsoleApi - axios parameter creator
* @export
*/
export const AttachmentV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {UploadFromUrlRequest} uploadFromUrlRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
externalTransferAttachment: async (uploadFromUrlRequest: UploadFromUrlRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'uploadFromUrlRequest' is not null or undefined
assertParamExists('externalTransferAttachment', 'uploadFromUrlRequest', uploadFromUrlRequest)
const localVarPath = `/apis/api.console.halo.run/v1alpha1/attachments/-/upload-from-url`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication basicAuth required
// http basic authentication required
setBasicAuthToObject(localVarRequestOptions, configuration)
// authentication bearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(uploadFromUrlRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {number} [page] Page number. Default is 0.
@ -178,6 +223,18 @@ export const AttachmentV1alpha1ConsoleApiAxiosParamCreator = function (configura
export const AttachmentV1alpha1ConsoleApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = AttachmentV1alpha1ConsoleApiAxiosParamCreator(configuration)
return {
/**
*
* @param {UploadFromUrlRequest} uploadFromUrlRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async externalTransferAttachment(uploadFromUrlRequest: UploadFromUrlRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.externalTransferAttachment(uploadFromUrlRequest, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1ConsoleApi.externalTransferAttachment']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {number} [page] Page number. Default is 0.
@ -221,6 +278,15 @@ export const AttachmentV1alpha1ConsoleApiFp = function(configuration?: Configura
export const AttachmentV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = AttachmentV1alpha1ConsoleApiFp(configuration)
return {
/**
*
* @param {AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
externalTransferAttachment(requestParameters: AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {
return localVarFp.externalTransferAttachment(requestParameters.uploadFromUrlRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @param {AttachmentV1alpha1ConsoleApiSearchAttachmentsRequest} requestParameters Request parameters.
@ -242,6 +308,20 @@ export const AttachmentV1alpha1ConsoleApiFactory = function (configuration?: Con
};
};
/**
* Request parameters for externalTransferAttachment operation in AttachmentV1alpha1ConsoleApi.
* @export
* @interface AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest
*/
export interface AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest {
/**
*
* @type {UploadFromUrlRequest}
* @memberof AttachmentV1alpha1ConsoleApiExternalTransferAttachment
*/
readonly uploadFromUrlRequest: UploadFromUrlRequest
}
/**
* Request parameters for searchAttachments operation in AttachmentV1alpha1ConsoleApi.
* @export
@ -340,6 +420,17 @@ export interface AttachmentV1alpha1ConsoleApiUploadAttachmentRequest {
* @extends {BaseAPI}
*/
export class AttachmentV1alpha1ConsoleApi extends BaseAPI {
/**
*
* @param {AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AttachmentV1alpha1ConsoleApi
*/
public externalTransferAttachment(requestParameters: AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest, options?: RawAxiosRequestConfig) {
return AttachmentV1alpha1ConsoleApiFp(this.configuration).externalTransferAttachment(requestParameters.uploadFromUrlRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {AttachmentV1alpha1ConsoleApiSearchAttachmentsRequest} requestParameters Request parameters.

View File

@ -23,6 +23,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import { Attachment } from '../models';
// @ts-ignore
import { UploadFromUrlRequest } from '../models';
/**
* AttachmentV1alpha1UcApi - axios parameter creator
* @export
@ -87,6 +89,54 @@ export const AttachmentV1alpha1UcApiAxiosParamCreator = function (configuration?
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = localVarFormParams;
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Upload attachment from the given URL.
* @param {UploadFromUrlRequest} uploadFromUrlRequest
* @param {boolean} [waitForPermalink] Wait for permalink.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
externalTransferAttachment1: async (uploadFromUrlRequest: UploadFromUrlRequest, waitForPermalink?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'uploadFromUrlRequest' is not null or undefined
assertParamExists('externalTransferAttachment1', 'uploadFromUrlRequest', uploadFromUrlRequest)
const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/attachments/-/upload-from-url`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication basicAuth required
// http basic authentication required
setBasicAuthToObject(localVarRequestOptions, configuration)
// authentication bearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (waitForPermalink !== undefined) {
localVarQueryParameter['waitForPermalink'] = waitForPermalink;
}
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(uploadFromUrlRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@ -117,6 +167,19 @@ export const AttachmentV1alpha1UcApiFp = function(configuration?: Configuration)
const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1UcApi.createAttachmentForPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Upload attachment from the given URL.
* @param {UploadFromUrlRequest} uploadFromUrlRequest
* @param {boolean} [waitForPermalink] Wait for permalink.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async externalTransferAttachment1(uploadFromUrlRequest: UploadFromUrlRequest, waitForPermalink?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.externalTransferAttachment1(uploadFromUrlRequest, waitForPermalink, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1UcApi.externalTransferAttachment1']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
@ -136,6 +199,15 @@ export const AttachmentV1alpha1UcApiFactory = function (configuration?: Configur
createAttachmentForPost(requestParameters: AttachmentV1alpha1UcApiCreateAttachmentForPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {
return localVarFp.createAttachmentForPost(requestParameters.file, requestParameters.waitForPermalink, requestParameters.postName, requestParameters.singlePageName, options).then((request) => request(axios, basePath));
},
/**
* Upload attachment from the given URL.
* @param {AttachmentV1alpha1UcApiExternalTransferAttachment1Request} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
externalTransferAttachment1(requestParameters: AttachmentV1alpha1UcApiExternalTransferAttachment1Request, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {
return localVarFp.externalTransferAttachment1(requestParameters.uploadFromUrlRequest, requestParameters.waitForPermalink, options).then((request) => request(axios, basePath));
},
};
};
@ -174,6 +246,27 @@ export interface AttachmentV1alpha1UcApiCreateAttachmentForPostRequest {
readonly singlePageName?: string
}
/**
* Request parameters for externalTransferAttachment1 operation in AttachmentV1alpha1UcApi.
* @export
* @interface AttachmentV1alpha1UcApiExternalTransferAttachment1Request
*/
export interface AttachmentV1alpha1UcApiExternalTransferAttachment1Request {
/**
*
* @type {UploadFromUrlRequest}
* @memberof AttachmentV1alpha1UcApiExternalTransferAttachment1
*/
readonly uploadFromUrlRequest: UploadFromUrlRequest
/**
* Wait for permalink.
* @type {boolean}
* @memberof AttachmentV1alpha1UcApiExternalTransferAttachment1
*/
readonly waitForPermalink?: boolean
}
/**
* AttachmentV1alpha1UcApi - object-oriented interface
* @export
@ -191,5 +284,16 @@ export class AttachmentV1alpha1UcApi extends BaseAPI {
public createAttachmentForPost(requestParameters: AttachmentV1alpha1UcApiCreateAttachmentForPostRequest, options?: RawAxiosRequestConfig) {
return AttachmentV1alpha1UcApiFp(this.configuration).createAttachmentForPost(requestParameters.file, requestParameters.waitForPermalink, requestParameters.postName, requestParameters.singlePageName, options).then((request) => request(this.axios, this.basePath));
}
/**
* Upload attachment from the given URL.
* @param {AttachmentV1alpha1UcApiExternalTransferAttachment1Request} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AttachmentV1alpha1UcApi
*/
public externalTransferAttachment1(requestParameters: AttachmentV1alpha1UcApiExternalTransferAttachment1Request, options?: RawAxiosRequestConfig) {
return AttachmentV1alpha1UcApiFp(this.configuration).externalTransferAttachment1(requestParameters.uploadFromUrlRequest, requestParameters.waitForPermalink, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -236,6 +236,7 @@ export * from './totp-auth-link-response';
export * from './totp-request';
export * from './two-factor-auth-settings';
export * from './upgrade-from-uri-request';
export * from './upload-from-url-request';
export * from './user';
export * from './user-connection';
export * from './user-connection-list';

View File

@ -0,0 +1,36 @@
/* tslint:disable */
/* eslint-disable */
/**
* Halo
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 2.19.0-SNAPSHOT
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
* @interface UploadFromUrlRequest
*/
export interface UploadFromUrlRequest {
/**
*
* @type {string}
* @memberof UploadFromUrlRequest
*/
'filename'?: string;
/**
*
* @type {string}
* @memberof UploadFromUrlRequest
*/
'url': string;
}