release: v1.4.3
GitHub Release / release (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s
Add a Telegram channel or group storage backend, with proxied image URLs and an optional API proxy.
This commit is contained in:
1 parent
4ed4692f5e
commit
cc19dc6435
32 files changed
+1527
-143
No files matched your search
@@ -21,13 +21,17 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const typeIcon = computed(() =>
|
||||
props.backend.type === 'local' ? 'i-lucide-hard-drive' : 'i-lucide-cloud'
|
||||
)
|
||||
const typeIcon = computed(() => {
|
||||
if (props.backend.type === 'local') return 'i-lucide-hard-drive'
|
||||
if (props.backend.type === 'telegram') return 'i-lucide-send'
|
||||
return 'i-lucide-cloud'
|
||||
})
|
||||
|
||||
const typeLabel = computed(() =>
|
||||
props.backend.type === 'local' ? t('storage.backendLocal') : t('storage.backendCloud')
|
||||
)
|
||||
const typeLabel = computed(() => {
|
||||
if (props.backend.type === 'local') return t('storage.backendLocal')
|
||||
if (props.backend.type === 'telegram') return t('storage.backendTelegram')
|
||||
return t('storage.backendCloud')
|
||||
})
|
||||
|
||||
const actionsDisabled = computed(() => props.envOverride || props.busy)
|
||||
|
||||
@@ -38,7 +42,7 @@ function canSetDefault() {
|
||||
}
|
||||
|
||||
function canEdit() {
|
||||
return props.backend.type === 's3'
|
||||
return props.backend.type === 's3' || props.backend.type === 'telegram'
|
||||
}
|
||||
|
||||
function canToggle() {
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import type { QuotaUnit } from '~/composables/useFileSize'
|
||||
import type { StorageBackendItem } from '~/types/storage'
|
||||
|
||||
type StorageProvider = 'r2' | 'cos' | 'oss' | 'aws' | 'custom'
|
||||
type StorageProvider = 'r2' | 'cos' | 'oss' | 'aws' | 'custom' | 'telegram'
|
||||
|
||||
export interface StorageFormPayload {
|
||||
backendType: 's3' | 'telegram'
|
||||
name: string
|
||||
provider: StorageProvider
|
||||
config: {
|
||||
@@ -13,10 +14,13 @@ export interface StorageFormPayload {
|
||||
bucket: string
|
||||
prefix?: string
|
||||
forcePathStyle: boolean
|
||||
chatId?: string
|
||||
apiProxyUrl?: string
|
||||
}
|
||||
secrets: {
|
||||
accessKeyId: string
|
||||
secretAccessKey: string
|
||||
botToken: string
|
||||
}
|
||||
servingMode: 'proxy' | 'public'
|
||||
publicUrl: string
|
||||
@@ -47,11 +51,18 @@ const prefix = ref('')
|
||||
const forcePathStyle = ref(false)
|
||||
const accessKeyId = ref('')
|
||||
const secretAccessKey = ref('')
|
||||
const chatId = ref('')
|
||||
const botToken = ref('')
|
||||
const apiProxyUrl = ref('')
|
||||
const servingMode = ref<'proxy' | 'public'>('proxy')
|
||||
const publicUrl = ref('')
|
||||
const quotaValue = ref('')
|
||||
const quotaUnit = ref<QuotaUnit>('GB')
|
||||
|
||||
const isTelegram = computed(() =>
|
||||
provider.value === 'telegram' || props.backend?.type === 'telegram'
|
||||
)
|
||||
|
||||
const quotaUnitOptions = computed(() =>
|
||||
QUOTA_UNITS.map(unit => ({ label: unit, value: unit }))
|
||||
)
|
||||
@@ -61,7 +72,8 @@ const providerOptions = computed(() => [
|
||||
{ label: t('storage.providerCos'), value: 'cos' as const },
|
||||
{ label: t('storage.providerOss'), value: 'oss' as const },
|
||||
{ label: t('storage.providerAws'), value: 'aws' as const },
|
||||
{ label: t('storage.providerCustom'), value: 'custom' as const }
|
||||
{ label: t('storage.providerCustom'), value: 'custom' as const },
|
||||
{ label: t('storage.providerTelegram'), value: 'telegram' as const }
|
||||
])
|
||||
|
||||
const servingModeOptions = computed(() => [
|
||||
@@ -79,6 +91,10 @@ function applyProviderTemplate(value: StorageProvider) {
|
||||
if (props.createMode) {
|
||||
name.value = providerLabel(value)
|
||||
}
|
||||
if (value === 'telegram') {
|
||||
servingMode.value = 'proxy'
|
||||
return
|
||||
}
|
||||
switch (value) {
|
||||
case 'r2':
|
||||
endpoint.value = 'https://{account_id}.r2.cloudflarestorage.com'
|
||||
@@ -114,21 +130,44 @@ function detectProvider(config: StorageBackendItem['config']): StorageProvider {
|
||||
return 'custom'
|
||||
}
|
||||
|
||||
function resetCreateDefaults() {
|
||||
name.value = ''
|
||||
provider.value = 'r2'
|
||||
applyProviderTemplate('r2')
|
||||
bucket.value = ''
|
||||
prefix.value = ''
|
||||
accessKeyId.value = ''
|
||||
secretAccessKey.value = ''
|
||||
chatId.value = ''
|
||||
botToken.value = ''
|
||||
apiProxyUrl.value = ''
|
||||
servingMode.value = 'proxy'
|
||||
publicUrl.value = ''
|
||||
quotaValue.value = ''
|
||||
quotaUnit.value = 'GB'
|
||||
}
|
||||
|
||||
function loadBackend(backend: StorageBackendItem | null | undefined) {
|
||||
if (!backend || backend.type !== 's3') {
|
||||
if (props.createMode) {
|
||||
name.value = ''
|
||||
provider.value = 'r2'
|
||||
applyProviderTemplate('r2')
|
||||
bucket.value = ''
|
||||
prefix.value = ''
|
||||
accessKeyId.value = ''
|
||||
secretAccessKey.value = ''
|
||||
servingMode.value = 'proxy'
|
||||
publicUrl.value = ''
|
||||
quotaValue.value = ''
|
||||
quotaUnit.value = 'GB'
|
||||
}
|
||||
if (!backend) {
|
||||
if (props.createMode) resetCreateDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
if (backend.type === 'telegram') {
|
||||
name.value = backend.name
|
||||
provider.value = 'telegram'
|
||||
chatId.value = backend.config.chatId ?? ''
|
||||
apiProxyUrl.value = backend.config.apiProxyUrl ?? ''
|
||||
botToken.value = ''
|
||||
const quota = bytesToQuotaInput(backend.quotaBytes)
|
||||
quotaValue.value = quota.value
|
||||
quotaUnit.value = quota.unit
|
||||
servingMode.value = 'proxy'
|
||||
return
|
||||
}
|
||||
|
||||
if (backend.type !== 's3') {
|
||||
if (props.createMode) resetCreateDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -157,7 +196,9 @@ watch(provider, (value) => {
|
||||
})
|
||||
|
||||
function handleSubmit() {
|
||||
const backendType = isTelegram.value ? 'telegram' : 's3'
|
||||
emit('submit', {
|
||||
backendType,
|
||||
name: name.value.trim() || providerLabel(provider.value),
|
||||
provider: provider.value,
|
||||
config: {
|
||||
@@ -165,13 +206,16 @@ function handleSubmit() {
|
||||
region: region.value.trim() || 'auto',
|
||||
bucket: bucket.value.trim(),
|
||||
prefix: prefix.value.trim() || undefined,
|
||||
forcePathStyle: forcePathStyle.value
|
||||
forcePathStyle: forcePathStyle.value,
|
||||
chatId: chatId.value.trim() || undefined,
|
||||
apiProxyUrl: apiProxyUrl.value.trim() || undefined
|
||||
},
|
||||
secrets: {
|
||||
accessKeyId: accessKeyId.value.trim(),
|
||||
secretAccessKey: secretAccessKey.value.trim()
|
||||
secretAccessKey: secretAccessKey.value.trim(),
|
||||
botToken: botToken.value.trim()
|
||||
},
|
||||
servingMode: servingMode.value,
|
||||
servingMode: isTelegram.value ? 'proxy' : servingMode.value,
|
||||
publicUrl: publicUrl.value.trim(),
|
||||
quotaValue: String(quotaValue.value ?? '').trim(),
|
||||
quotaUnit: quotaUnit.value
|
||||
@@ -190,6 +234,7 @@ function handleSubmit() {
|
||||
v-model="provider"
|
||||
:items="providerOptions"
|
||||
class="w-full"
|
||||
:disabled="!createMode && backend?.type === 'telegram'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -201,105 +246,158 @@ function handleSubmit() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.endpoint') }}</label>
|
||||
<UInput
|
||||
v-model="endpoint"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.region') }}</label>
|
||||
<UInput
|
||||
v-model="region"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="isTelegram">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">
|
||||
{{ t('storage.bucket') }}
|
||||
{{ t('storage.telegramChatId') }}
|
||||
<span class="text-error">*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="bucket"
|
||||
v-model="chatId"
|
||||
:placeholder="t('storage.telegramChatIdPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
required
|
||||
/>
|
||||
<p class="text-xs text-muted">
|
||||
{{ t('storage.telegramChatIdHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.prefix') }}</label>
|
||||
<UInput
|
||||
v-model="prefix"
|
||||
:placeholder="t('storage.prefixPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="flex cursor-pointer items-center gap-2">
|
||||
<UCheckbox v-model="forcePathStyle" />
|
||||
<span class="text-sm">{{ t('storage.forcePathStyle') }}</span>
|
||||
</label>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">
|
||||
{{ t('storage.accessKey') }}
|
||||
{{ t('storage.telegramBotToken') }}
|
||||
<span
|
||||
v-if="createMode"
|
||||
class="text-error"
|
||||
>*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="accessKeyId"
|
||||
:placeholder="backend?.secretsMasked.accessKeyId || ''"
|
||||
class="w-full font-mono text-sm"
|
||||
:required="createMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">
|
||||
{{ t('storage.secretKey') }}
|
||||
<span
|
||||
v-if="createMode"
|
||||
class="text-error"
|
||||
>*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="secretAccessKey"
|
||||
v-model="botToken"
|
||||
type="password"
|
||||
:placeholder="backend?.secretsMasked.secretAccessKey || ''"
|
||||
:placeholder="backend?.secretsMasked.botToken || ''"
|
||||
class="w-full font-mono text-sm"
|
||||
:required="createMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.servingMode') }}</label>
|
||||
<URadioGroup
|
||||
v-model="servingMode"
|
||||
:items="servingModeOptions"
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.telegramApiProxy') }}</label>
|
||||
<UInput
|
||||
v-model="apiProxyUrl"
|
||||
:placeholder="t('storage.telegramApiProxyPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
<p class="text-xs text-muted">
|
||||
{{ t('storage.telegramApiProxyHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-muted">
|
||||
{{ t('storage.servingHint') }}
|
||||
{{ t('storage.telegramServingHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="servingMode === 'public'"
|
||||
class="space-y-2"
|
||||
>
|
||||
<label class="text-sm">{{ t('storage.publicUrl') }}</label>
|
||||
<UInput
|
||||
v-model="publicUrl"
|
||||
:placeholder="t('storage.publicUrlPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.endpoint') }}</label>
|
||||
<UInput
|
||||
v-model="endpoint"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.region') }}</label>
|
||||
<UInput
|
||||
v-model="region"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">
|
||||
{{ t('storage.bucket') }}
|
||||
<span class="text-error">*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="bucket"
|
||||
class="w-full font-mono text-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.prefix') }}</label>
|
||||
<UInput
|
||||
v-model="prefix"
|
||||
:placeholder="t('storage.prefixPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="flex cursor-pointer items-center gap-2">
|
||||
<UCheckbox v-model="forcePathStyle" />
|
||||
<span class="text-sm">{{ t('storage.forcePathStyle') }}</span>
|
||||
</label>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">
|
||||
{{ t('storage.accessKey') }}
|
||||
<span
|
||||
v-if="createMode"
|
||||
class="text-error"
|
||||
>*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="accessKeyId"
|
||||
:placeholder="backend?.secretsMasked.accessKeyId || ''"
|
||||
class="w-full font-mono text-sm"
|
||||
:required="createMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">
|
||||
{{ t('storage.secretKey') }}
|
||||
<span
|
||||
v-if="createMode"
|
||||
class="text-error"
|
||||
>*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="secretAccessKey"
|
||||
type="password"
|
||||
:placeholder="backend?.secretsMasked.secretAccessKey || ''"
|
||||
class="w-full font-mono text-sm"
|
||||
:required="createMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{{ t('storage.servingMode') }}</label>
|
||||
<URadioGroup
|
||||
v-model="servingMode"
|
||||
:items="servingModeOptions"
|
||||
/>
|
||||
<p class="text-xs text-muted">
|
||||
{{ t('storage.servingHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="servingMode === 'public'"
|
||||
class="space-y-2"
|
||||
>
|
||||
<label class="text-sm">{{ t('storage.publicUrl') }}</label>
|
||||
<UInput
|
||||
v-model="publicUrl"
|
||||
:placeholder="t('storage.publicUrlPlaceholder')"
|
||||
class="w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">
|
||||
@@ -324,7 +422,7 @@ function handleSubmit() {
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-muted">
|
||||
{{ t('storage.quotaHint') }}
|
||||
{{ isTelegram ? t('storage.telegramQuotaHint') : t('storage.quotaHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@ const items = computed(() => [
|
||||
boxClass: 'border-blue-500/20 bg-blue-500/5',
|
||||
title: t('storage.supportedS3Title'),
|
||||
desc: t('storage.supportedS3Desc')
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-send',
|
||||
iconClass: 'text-sky-500',
|
||||
boxClass: 'border-sky-500/20 bg-sky-500/5',
|
||||
title: t('storage.backendTelegram'),
|
||||
desc: t('storage.supportedTelegramDesc')
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user