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
Ryan Wang 2025-07-04 12:37:40 +08:00 committed by GitHub
parent 79226998d3
commit a4a418b22e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 337 additions and 92 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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"
} }
} }
}, },

View File

@ -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"
} }
} }
}, },

View File

@ -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",

View File

@ -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;

View File

@ -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"

View File

@ -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.

View File

@ -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.");

View File

@ -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.");

View File

@ -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));

View File

@ -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);
});
}
} }
} }

View File

@ -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);
} }

View File

@ -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));

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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));
} }
/** /**

View File

@ -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';

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.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;
}

View File

@ -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}

View File

@ -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.

View File

@ -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:

View File

@ -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:

View File

@ -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: