mirror of https://github.com/halo-dev/halo
feat: add support for remote URL attachment downloads (#7602)
#### What type of PR is this? /area ui /kind feature /milestone 2.21.x #### What this PR does / why we need it: Add support for remote URL attachment downloads <img width="1031" alt="image" src="https://github.com/user-attachments/assets/f85eee2f-a40b-49ff-9ced-31136f59e67c" /> #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/7017 #### Does this PR introduce a user-facing change? ```release-note 支持通过远程地址下载到附件库 ```pull/7613/head
parent
79226998d3
commit
a4a418b22e
|
@ -15420,7 +15420,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UploadFromUrlRequest"
|
"$ref": "#/components/schemas/UcUploadFromUrlRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -20583,12 +20583,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -22355,12 +22355,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -23386,6 +23386,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"UcUploadFromUrlRequest": {
|
||||||
|
"required": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filename": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Custom file name"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"UcUploadRequest": {
|
"UcUploadRequest": {
|
||||||
"required": [
|
"required": [
|
||||||
"file"
|
"file"
|
||||||
|
@ -23445,12 +23461,22 @@
|
||||||
},
|
},
|
||||||
"UploadFromUrlRequest": {
|
"UploadFromUrlRequest": {
|
||||||
"required": [
|
"required": [
|
||||||
|
"policyName",
|
||||||
"url"
|
"url"
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"filename": {
|
"filename": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Custom file name"
|
||||||
|
},
|
||||||
|
"groupName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the group to which the attachment belongs"
|
||||||
|
},
|
||||||
|
"policyName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Storage policy name"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
@ -5471,12 +5471,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6007,12 +6007,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6468,13 +6468,16 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"filename": {
|
"filename": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Custom file name"
|
||||||
},
|
},
|
||||||
"groupName": {
|
"groupName": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "The name of the group to which the attachment belongs"
|
||||||
},
|
},
|
||||||
"policyName": {
|
"policyName": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Storage policy name"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
@ -12903,12 +12903,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -14281,12 +14281,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2609,12 +2609,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3183,12 +3183,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1420,7 +1420,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UploadFromUrlRequest"
|
"$ref": "#/components/schemas/UcUploadFromUrlRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2470,12 +2470,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2911,6 +2911,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"UcUploadFromUrlRequest": {
|
||||||
|
"required": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filename": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Custom file name"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"UcUploadRequest": {
|
"UcUploadRequest": {
|
||||||
"required": [
|
"required": [
|
||||||
"file"
|
"file"
|
||||||
|
@ -2944,21 +2960,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"UploadFromUrlRequest": {
|
|
||||||
"required": [
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"filename": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "url"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"UserConnection": {
|
"UserConnection": {
|
||||||
"required": [
|
"required": [
|
||||||
"apiVersion",
|
"apiVersion",
|
||||||
|
|
|
@ -5,12 +5,10 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import run.halo.app.extension.AbstractExtension;
|
import run.halo.app.extension.AbstractExtension;
|
||||||
import run.halo.app.extension.GVK;
|
import run.halo.app.extension.GVK;
|
||||||
|
|
|
@ -73,7 +73,7 @@ public class FileTypeDetectUtils {
|
||||||
/**
|
/**
|
||||||
* <p>Recommend to use this method to verify whether the file extension matches the file type
|
* <p>Recommend to use this method to verify whether the file extension matches the file type
|
||||||
* after matching the file type to avoid XSS attacks such as bypassing detection by polyglot
|
* after matching the file type to avoid XSS attacks such as bypassing detection by polyglot
|
||||||
* file</p>
|
* file.</p>
|
||||||
*
|
*
|
||||||
* @param mimeType file mime type,such as "image/png"
|
* @param mimeType file mime type,such as "image/png"
|
||||||
* @param fileName file name,such as "test.png"
|
* @param fileName file name,such as "test.png"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package run.halo.app.theme.router;
|
package run.halo.app.theme.router;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.math.NumberUtils;
|
import org.apache.commons.lang3.math.NumberUtils;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
import run.halo.app.infra.utils.PathUtils;
|
import run.halo.app.infra.utils.PathUtils;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility class for template page url.
|
* A utility class for template page url.
|
||||||
|
|
|
@ -118,9 +118,11 @@ public class AttachmentEndpoint implements CustomEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,
|
public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,
|
||||||
@Schema(requiredMode = REQUIRED) String policyName,
|
@Schema(requiredMode = REQUIRED, description = "Storage "
|
||||||
String groupName,
|
+ "policy name") String policyName,
|
||||||
String filename) {
|
@Schema(description = "The name of the group to which the "
|
||||||
|
+ "attachment belongs") String groupName,
|
||||||
|
@Schema(description = "Custom file name") String filename) {
|
||||||
public UploadFromUrlRequest {
|
public UploadFromUrlRequest {
|
||||||
if (Objects.isNull(url)) {
|
if (Objects.isNull(url)) {
|
||||||
throw new ServerWebInputException("Required url is missing.");
|
throw new ServerWebInputException("Required url is missing.");
|
||||||
|
|
|
@ -333,8 +333,9 @@ public class UcAttachmentEndpoint implements CustomEndpoint {
|
||||||
return GroupVersion.parseAPIVersion("uc.api.storage.halo.run/v1alpha1");
|
return GroupVersion.parseAPIVersion("uc.api.storage.halo.run/v1alpha1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Schema(name = "UcUploadFromUrlRequest")
|
||||||
public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,
|
public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,
|
||||||
String filename) {
|
@Schema(description = "Custom file name") String filename) {
|
||||||
public UploadFromUrlRequest {
|
public UploadFromUrlRequest {
|
||||||
if (Objects.isNull(url)) {
|
if (Objects.isNull(url)) {
|
||||||
throw new ServerWebInputException("Required url is missing.");
|
throw new ServerWebInputException("Required url is missing.");
|
||||||
|
|
|
@ -147,14 +147,13 @@ public class DefaultAttachmentService implements AttachmentService {
|
||||||
AtomicReference<String> fileNameRef = new AtomicReference<>(filename);
|
AtomicReference<String> fileNameRef = new AtomicReference<>(filename);
|
||||||
|
|
||||||
Mono<Flux<DataBuffer>> contentMono = dataBufferFetcher.head(uri)
|
Mono<Flux<DataBuffer>> contentMono = dataBufferFetcher.head(uri)
|
||||||
.map(response -> {
|
.map(httpHeaders -> {
|
||||||
var httpHeaders = response.getHeaders();
|
|
||||||
if (!StringUtils.hasText(fileNameRef.get())) {
|
if (!StringUtils.hasText(fileNameRef.get())) {
|
||||||
fileNameRef.set(getExternalUrlFilename(uri, httpHeaders));
|
fileNameRef.set(getExternalUrlFilename(uri, httpHeaders));
|
||||||
}
|
}
|
||||||
MediaType contentType = httpHeaders.getContentType();
|
MediaType contentType = httpHeaders.getContentType();
|
||||||
mediaTypeRef.set(contentType);
|
mediaTypeRef.set(contentType);
|
||||||
return response;
|
return httpHeaders;
|
||||||
})
|
})
|
||||||
.map(response -> dataBufferFetcher.fetch(uri));
|
.map(response -> dataBufferFetcher.fetch(uri));
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,11 @@ package run.halo.app.infra;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
@ -21,6 +22,8 @@ import reactor.netty.http.client.HttpClient;
|
||||||
public class DefaultReactiveUrlDataBufferFetcher implements ReactiveUrlDataBufferFetcher {
|
public class DefaultReactiveUrlDataBufferFetcher implements ReactiveUrlDataBufferFetcher {
|
||||||
private final HttpClient httpClient = HttpClient.create()
|
private final HttpClient httpClient = HttpClient.create()
|
||||||
.followRedirect(true);
|
.followRedirect(true);
|
||||||
|
private final ContentLengthFetcher contentLengthFetcher = new ContentLengthFetcher();
|
||||||
|
|
||||||
private final WebClient webClient = WebClient.builder()
|
private final WebClient webClient = WebClient.builder()
|
||||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||||
.build();
|
.build();
|
||||||
|
@ -35,10 +38,32 @@ public class DefaultReactiveUrlDataBufferFetcher implements ReactiveUrlDataBuffe
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ResponseEntity<Void>> head(URI uri) {
|
public Mono<HttpHeaders> head(URI uri) {
|
||||||
return webClient.head()
|
return contentLengthFetcher.fetchContentLength(uri);
|
||||||
.uri(uri)
|
}
|
||||||
.retrieve()
|
|
||||||
.toBodilessEntity();
|
static class ContentLengthFetcher {
|
||||||
|
|
||||||
|
private final WebClient webClient;
|
||||||
|
|
||||||
|
ContentLengthFetcher() {
|
||||||
|
this.webClient = WebClient.builder()
|
||||||
|
.exchangeStrategies(ExchangeStrategies.builder()
|
||||||
|
.codecs(config -> config.defaultCodecs().maxInMemorySize(1))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<HttpHeaders> fetchContentLength(URI url) {
|
||||||
|
return webClient.get()
|
||||||
|
.uri(url)
|
||||||
|
.exchangeToMono(response -> {
|
||||||
|
HttpHeaders headers = response.headers().asHttpHeaders();
|
||||||
|
|
||||||
|
return response.bodyToMono(byte[].class)
|
||||||
|
.onErrorResume(ex -> Mono.empty())
|
||||||
|
.thenReturn(headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package run.halo.app.infra;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.HttpHeaders;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@ -28,5 +28,5 @@ public interface ReactiveUrlDataBufferFetcher {
|
||||||
* @param uri uri to fetch
|
* @param uri uri to fetch
|
||||||
* @return response entity
|
* @return response entity
|
||||||
*/
|
*/
|
||||||
Mono<ResponseEntity<Void>> head(URI uri);
|
Mono<HttpHeaders> head(URI uri);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatusCode;
|
import org.springframework.http.HttpStatusCode;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
@ -303,10 +304,11 @@ class AttachmentEndpointTest {
|
||||||
attachment.setMetadata(metadata);
|
attachment.setMetadata(metadata);
|
||||||
|
|
||||||
ResponseEntity<Void> response = new ResponseEntity<>(HttpStatusCode.valueOf(200));
|
ResponseEntity<Void> response = new ResponseEntity<>(HttpStatusCode.valueOf(200));
|
||||||
|
HttpHeaders headers = response.getHeaders();
|
||||||
DataBuffer dataBuffer = mock(DataBuffer.class);
|
DataBuffer dataBuffer = mock(DataBuffer.class);
|
||||||
|
|
||||||
when(handler.upload(any())).thenReturn(Mono.just(attachment));
|
when(handler.upload(any())).thenReturn(Mono.just(attachment));
|
||||||
when(dataBufferFetcher.head(any())).thenReturn(Mono.just(response));
|
when(dataBufferFetcher.head(any())).thenReturn(Mono.just(headers));
|
||||||
when(dataBufferFetcher.fetch(any())).thenReturn(Flux.just(dataBuffer));
|
when(dataBufferFetcher.fetch(any())).thenReturn(Flux.just(dataBuffer));
|
||||||
when(extensionGetter.getExtensions(AttachmentHandler.class))
|
when(extensionGetter.getExtensions(AttachmentHandler.class))
|
||||||
.thenReturn(Flux.just(handler));
|
.thenReturn(Flux.just(handler));
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {
|
||||||
VDropdown,
|
VDropdown,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
VModal,
|
VModal,
|
||||||
|
VTabItem,
|
||||||
|
VTabs,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
|
@ -18,6 +20,7 @@ import AttachmentGroupBadge from "./AttachmentGroupBadge.vue";
|
||||||
import AttachmentGroupEditingModal from "./AttachmentGroupEditingModal.vue";
|
import AttachmentGroupEditingModal from "./AttachmentGroupEditingModal.vue";
|
||||||
import AttachmentPolicyBadge from "./AttachmentPolicyBadge.vue";
|
import AttachmentPolicyBadge from "./AttachmentPolicyBadge.vue";
|
||||||
import AttachmentPolicyEditingModal from "./AttachmentPolicyEditingModal.vue";
|
import AttachmentPolicyEditingModal from "./AttachmentPolicyEditingModal.vue";
|
||||||
|
import UploadFromUrl from "./UploadFromUrl.vue";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
|
@ -62,6 +65,8 @@ const onGroupEditingModalClose = async () => {
|
||||||
await handleFetchGroups();
|
await handleFetchGroups();
|
||||||
groupEditingModal.value = false;
|
groupEditingModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const activeTab = ref("upload");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -156,22 +161,43 @@ const onGroupEditingModalClose = async () => {
|
||||||
</template>
|
</template>
|
||||||
</AttachmentGroupBadge>
|
</AttachmentGroupBadge>
|
||||||
</div>
|
</div>
|
||||||
<UppyUpload
|
|
||||||
endpoint="/apis/api.console.halo.run/v1alpha1/attachments/upload"
|
<div class="mb-3">
|
||||||
:disabled="!selectedPolicyName"
|
<VTabs v-model:active-id="activeTab" type="outline">
|
||||||
:meta="{
|
<VTabItem
|
||||||
policyName: selectedPolicyName,
|
id="upload"
|
||||||
groupName: selectedGroupName,
|
:label="
|
||||||
}"
|
$t('core.attachment.upload_modal.upload_options.local_upload')
|
||||||
width="100%"
|
"
|
||||||
:allowed-meta-fields="['policyName', 'groupName']"
|
>
|
||||||
:note="
|
<UppyUpload
|
||||||
selectedPolicyName
|
endpoint="/apis/api.console.halo.run/v1alpha1/attachments/upload"
|
||||||
? ''
|
:disabled="!selectedPolicyName"
|
||||||
: $t('core.attachment.upload_modal.filters.policy.not_select')
|
:meta="{
|
||||||
"
|
policyName: selectedPolicyName,
|
||||||
:done-button-handler="() => modal?.close()"
|
groupName: selectedGroupName,
|
||||||
/>
|
}"
|
||||||
|
width="100%"
|
||||||
|
:allowed-meta-fields="['policyName', 'groupName']"
|
||||||
|
:note="
|
||||||
|
selectedPolicyName
|
||||||
|
? ''
|
||||||
|
: $t('core.attachment.upload_modal.filters.policy.not_select')
|
||||||
|
"
|
||||||
|
:done-button-handler="() => modal?.close()"
|
||||||
|
/>
|
||||||
|
</VTabItem>
|
||||||
|
<VTabItem
|
||||||
|
id="download"
|
||||||
|
:label="$t('core.attachment.upload_modal.upload_options.download')"
|
||||||
|
>
|
||||||
|
<UploadFromUrl
|
||||||
|
:policy-name="selectedPolicyName"
|
||||||
|
:group-name="selectedGroupName"
|
||||||
|
/>
|
||||||
|
</VTabItem>
|
||||||
|
</VTabs>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VModal>
|
</VModal>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { setFocus } from "@/formkit/utils/focus";
|
||||||
|
import { reset } from "@formkit/core";
|
||||||
|
import { consoleApiClient } from "@halo-dev/api-client";
|
||||||
|
import { Toast, VButton } from "@halo-dev/components";
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
policyName: string;
|
||||||
|
groupName: string;
|
||||||
|
}>(),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setFocus("url");
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloading = ref(false);
|
||||||
|
|
||||||
|
async function onSubmit(data: { url: string }) {
|
||||||
|
try {
|
||||||
|
downloading.value = true;
|
||||||
|
|
||||||
|
await consoleApiClient.storage.attachment.externalTransferAttachment({
|
||||||
|
uploadFromUrlRequest: {
|
||||||
|
url: data.url,
|
||||||
|
policyName: props.policyName,
|
||||||
|
groupName: props.groupName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success(
|
||||||
|
t("core.attachment.upload_modal.download_form.toast.success")
|
||||||
|
);
|
||||||
|
|
||||||
|
reset("url");
|
||||||
|
} catch (error) {
|
||||||
|
return error;
|
||||||
|
} finally {
|
||||||
|
downloading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<FormKit
|
||||||
|
id="upload-from-url"
|
||||||
|
type="form"
|
||||||
|
name="upload-from-url"
|
||||||
|
:config="{ validationVisibility: 'submit' }"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<FormKit
|
||||||
|
id="url"
|
||||||
|
type="url"
|
||||||
|
name="url"
|
||||||
|
:label="$t('core.attachment.upload_modal.download_form.fields.url.label')"
|
||||||
|
:validation="[['required'], ['url']]"
|
||||||
|
/>
|
||||||
|
</FormKit>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<VButton
|
||||||
|
type="secondary"
|
||||||
|
:loading="downloading"
|
||||||
|
@click="$formkit.submit('upload-from-url')"
|
||||||
|
>
|
||||||
|
{{ $t("core.common.buttons.download") }}
|
||||||
|
</VButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -322,6 +322,7 @@ models/thumbnail.ts
|
||||||
models/totp-auth-link-response.ts
|
models/totp-auth-link-response.ts
|
||||||
models/totp-request.ts
|
models/totp-request.ts
|
||||||
models/two-factor-auth-settings.ts
|
models/two-factor-auth-settings.ts
|
||||||
|
models/uc-upload-from-url-request.ts
|
||||||
models/uc-upload-request-form-data.ts
|
models/uc-upload-request-form-data.ts
|
||||||
models/upgrade-from-uri-request.ts
|
models/upgrade-from-uri-request.ts
|
||||||
models/upload-from-url-request.ts
|
models/upload-from-url-request.ts
|
||||||
|
|
|
@ -26,9 +26,9 @@ import { Attachment } from '../models';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { AttachmentList } from '../models';
|
import { AttachmentList } from '../models';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { UcUploadRequestFormData } from '../models';
|
import { UcUploadFromUrlRequest } from '../models';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { UploadFromUrlRequest } from '../models';
|
import { UcUploadRequestFormData } from '../models';
|
||||||
/**
|
/**
|
||||||
* AttachmentV1alpha1UcApi - axios parameter creator
|
* AttachmentV1alpha1UcApi - axios parameter creator
|
||||||
* @export
|
* @export
|
||||||
|
@ -100,14 +100,14 @@ export const AttachmentV1alpha1UcApiAxiosParamCreator = function (configuration?
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Upload attachment from the given URL.
|
* Upload attachment from the given URL.
|
||||||
* @param {UploadFromUrlRequest} uploadFromUrlRequest
|
* @param {UcUploadFromUrlRequest} ucUploadFromUrlRequest
|
||||||
* @param {boolean} [waitForPermalink] Wait for permalink.
|
* @param {boolean} [waitForPermalink] Wait for permalink.
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
externalTransferAttachment1: async (uploadFromUrlRequest: UploadFromUrlRequest, waitForPermalink?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
externalTransferAttachment1: async (ucUploadFromUrlRequest: UcUploadFromUrlRequest, waitForPermalink?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'uploadFromUrlRequest' is not null or undefined
|
// verify required parameter 'ucUploadFromUrlRequest' is not null or undefined
|
||||||
assertParamExists('externalTransferAttachment1', 'uploadFromUrlRequest', uploadFromUrlRequest)
|
assertParamExists('externalTransferAttachment1', 'ucUploadFromUrlRequest', ucUploadFromUrlRequest)
|
||||||
const localVarPath = `/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload-from-url`;
|
const localVarPath = `/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload-from-url`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
@ -139,7 +139,7 @@ export const AttachmentV1alpha1UcApiAxiosParamCreator = function (configuration?
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
localVarRequestOptions.data = serializeDataIfNeeded(uploadFromUrlRequest, localVarRequestOptions, configuration)
|
localVarRequestOptions.data = serializeDataIfNeeded(ucUploadFromUrlRequest, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: toPathString(localVarUrlObj),
|
url: toPathString(localVarUrlObj),
|
||||||
|
@ -303,13 +303,13 @@ export const AttachmentV1alpha1UcApiFp = function(configuration?: Configuration)
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Upload attachment from the given URL.
|
* Upload attachment from the given URL.
|
||||||
* @param {UploadFromUrlRequest} uploadFromUrlRequest
|
* @param {UcUploadFromUrlRequest} ucUploadFromUrlRequest
|
||||||
* @param {boolean} [waitForPermalink] Wait for permalink.
|
* @param {boolean} [waitForPermalink] Wait for permalink.
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async externalTransferAttachment1(uploadFromUrlRequest: UploadFromUrlRequest, waitForPermalink?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {
|
async externalTransferAttachment1(ucUploadFromUrlRequest: UcUploadFromUrlRequest, waitForPermalink?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.externalTransferAttachment1(uploadFromUrlRequest, waitForPermalink, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.externalTransferAttachment1(ucUploadFromUrlRequest, waitForPermalink, options);
|
||||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||||
const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1UcApi.externalTransferAttachment1']?.[localVarOperationServerIndex]?.url;
|
const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1UcApi.externalTransferAttachment1']?.[localVarOperationServerIndex]?.url;
|
||||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||||
|
@ -372,7 +372,7 @@ export const AttachmentV1alpha1UcApiFactory = function (configuration?: Configur
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
externalTransferAttachment1(requestParameters: AttachmentV1alpha1UcApiExternalTransferAttachment1Request, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {
|
externalTransferAttachment1(requestParameters: AttachmentV1alpha1UcApiExternalTransferAttachment1Request, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {
|
||||||
return localVarFp.externalTransferAttachment1(requestParameters.uploadFromUrlRequest, requestParameters.waitForPermalink, options).then((request) => request(axios, basePath));
|
return localVarFp.externalTransferAttachment1(requestParameters.ucUploadFromUrlRequest, requestParameters.waitForPermalink, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* List attachments of the current user uploaded.
|
* List attachments of the current user uploaded.
|
||||||
|
@ -438,10 +438,10 @@ export interface AttachmentV1alpha1UcApiCreateAttachmentForPostRequest {
|
||||||
export interface AttachmentV1alpha1UcApiExternalTransferAttachment1Request {
|
export interface AttachmentV1alpha1UcApiExternalTransferAttachment1Request {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {UploadFromUrlRequest}
|
* @type {UcUploadFromUrlRequest}
|
||||||
* @memberof AttachmentV1alpha1UcApiExternalTransferAttachment1
|
* @memberof AttachmentV1alpha1UcApiExternalTransferAttachment1
|
||||||
*/
|
*/
|
||||||
readonly uploadFromUrlRequest: UploadFromUrlRequest
|
readonly ucUploadFromUrlRequest: UcUploadFromUrlRequest
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for permalink.
|
* Wait for permalink.
|
||||||
|
@ -561,7 +561,7 @@ export class AttachmentV1alpha1UcApi extends BaseAPI {
|
||||||
* @memberof AttachmentV1alpha1UcApi
|
* @memberof AttachmentV1alpha1UcApi
|
||||||
*/
|
*/
|
||||||
public externalTransferAttachment1(requestParameters: AttachmentV1alpha1UcApiExternalTransferAttachment1Request, options?: RawAxiosRequestConfig) {
|
public externalTransferAttachment1(requestParameters: AttachmentV1alpha1UcApiExternalTransferAttachment1Request, options?: RawAxiosRequestConfig) {
|
||||||
return AttachmentV1alpha1UcApiFp(this.configuration).externalTransferAttachment1(requestParameters.uploadFromUrlRequest, requestParameters.waitForPermalink, options).then((request) => request(this.axios, this.basePath));
|
return AttachmentV1alpha1UcApiFp(this.configuration).externalTransferAttachment1(requestParameters.ucUploadFromUrlRequest, requestParameters.waitForPermalink, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -233,6 +233,7 @@ export * from './thumbnail-spec';
|
||||||
export * from './totp-auth-link-response';
|
export * from './totp-auth-link-response';
|
||||||
export * from './totp-request';
|
export * from './totp-request';
|
||||||
export * from './two-factor-auth-settings';
|
export * from './two-factor-auth-settings';
|
||||||
|
export * from './uc-upload-from-url-request';
|
||||||
export * from './uc-upload-request-form-data';
|
export * from './uc-upload-request-form-data';
|
||||||
export * from './upgrade-from-uri-request';
|
export * from './upgrade-from-uri-request';
|
||||||
export * from './upload-from-url-request';
|
export * from './upload-from-url-request';
|
||||||
|
|
|
@ -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.21.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 UcUploadFromUrlRequest
|
||||||
|
*/
|
||||||
|
export interface UcUploadFromUrlRequest {
|
||||||
|
/**
|
||||||
|
* Custom file name
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UcUploadFromUrlRequest
|
||||||
|
*/
|
||||||
|
'filename'?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UcUploadFromUrlRequest
|
||||||
|
*/
|
||||||
|
'url': string;
|
||||||
|
}
|
||||||
|
|
|
@ -21,11 +21,23 @@
|
||||||
*/
|
*/
|
||||||
export interface UploadFromUrlRequest {
|
export interface UploadFromUrlRequest {
|
||||||
/**
|
/**
|
||||||
*
|
* Custom file name
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof UploadFromUrlRequest
|
* @memberof UploadFromUrlRequest
|
||||||
*/
|
*/
|
||||||
'filename'?: string;
|
'filename'?: string;
|
||||||
|
/**
|
||||||
|
* The name of the group to which the attachment belongs
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UploadFromUrlRequest
|
||||||
|
*/
|
||||||
|
'groupName'?: string;
|
||||||
|
/**
|
||||||
|
* Storage policy name
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UploadFromUrlRequest
|
||||||
|
*/
|
||||||
|
'policyName': string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
|
|
@ -222,6 +222,16 @@ core:
|
||||||
policy_editing_modal:
|
policy_editing_modal:
|
||||||
toast:
|
toast:
|
||||||
policy_name_exists: Storage policy name already exists
|
policy_name_exists: Storage policy name already exists
|
||||||
|
upload_modal:
|
||||||
|
upload_options:
|
||||||
|
local_upload: Upload
|
||||||
|
download: Download from url
|
||||||
|
download_form:
|
||||||
|
fields:
|
||||||
|
url:
|
||||||
|
label: URL
|
||||||
|
toast:
|
||||||
|
success: Downloaded successfully
|
||||||
uc_attachment:
|
uc_attachment:
|
||||||
empty:
|
empty:
|
||||||
title: There are no attachments.
|
title: There are no attachments.
|
||||||
|
|
|
@ -158,8 +158,8 @@ core:
|
||||||
back:
|
back:
|
||||||
title: Layout not saved
|
title: Layout not saved
|
||||||
description: >-
|
description: >-
|
||||||
The current layout has not been saved, if you leave, the current layout
|
The current layout has not been saved, if you leave, the current
|
||||||
will be lost, do you want to continue?
|
layout will be lost, do you want to continue?
|
||||||
confirm_text: Leave
|
confirm_text: Leave
|
||||||
change_breakpoint:
|
change_breakpoint:
|
||||||
tips_not_saved: Please save the current layout first
|
tips_not_saved: Please save the current layout first
|
||||||
|
@ -754,6 +754,15 @@ core:
|
||||||
title: No storage policy
|
title: No storage policy
|
||||||
description: Before uploading, a new storage policy needs to be created.
|
description: Before uploading, a new storage policy needs to be created.
|
||||||
not_select: Please select a storage policy first.
|
not_select: Please select a storage policy first.
|
||||||
|
upload_options:
|
||||||
|
local_upload: Upload
|
||||||
|
download: Download from url
|
||||||
|
download_form:
|
||||||
|
fields:
|
||||||
|
url:
|
||||||
|
label: URL
|
||||||
|
toast:
|
||||||
|
success: Downloaded successfully
|
||||||
select_modal:
|
select_modal:
|
||||||
title: Select attachment
|
title: Select attachment
|
||||||
providers:
|
providers:
|
||||||
|
|
|
@ -711,6 +711,15 @@ core:
|
||||||
title: 没有存储策略
|
title: 没有存储策略
|
||||||
description: 在上传之前,需要新建一个存储策略
|
description: 在上传之前,需要新建一个存储策略
|
||||||
not_select: 请先选择存储策略
|
not_select: 请先选择存储策略
|
||||||
|
upload_options:
|
||||||
|
local_upload: 本地上传
|
||||||
|
download: 通过链接下载
|
||||||
|
download_form:
|
||||||
|
fields:
|
||||||
|
url:
|
||||||
|
label: 链接地址
|
||||||
|
toast:
|
||||||
|
success: 下载成功
|
||||||
select_modal:
|
select_modal:
|
||||||
title: 选择附件
|
title: 选择附件
|
||||||
providers:
|
providers:
|
||||||
|
|
|
@ -696,6 +696,15 @@ core:
|
||||||
title: 沒有存儲策略
|
title: 沒有存儲策略
|
||||||
description: 在上傳之前,需要新建一個存儲策略
|
description: 在上傳之前,需要新建一個存儲策略
|
||||||
not_select: 請先選擇存儲策略
|
not_select: 請先選擇存儲策略
|
||||||
|
upload_options:
|
||||||
|
local_upload: 本地上傳
|
||||||
|
download: 通過鏈接下載
|
||||||
|
download_form:
|
||||||
|
fields:
|
||||||
|
url:
|
||||||
|
label: 鏈接地址
|
||||||
|
toast:
|
||||||
|
success: 下載成功
|
||||||
select_modal:
|
select_modal:
|
||||||
title: 選擇附件
|
title: 選擇附件
|
||||||
providers:
|
providers:
|
||||||
|
|
Loading…
Reference in New Issue