feat: WebDAV 支持连接 Alist (#3836)

Refs #3495
This commit is contained in:
ssongliu 2024-02-05 17:28:12 +08:00 committed by GitHub
parent 9e980e8e0c
commit d8669b90bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 65 additions and 53 deletions

View file

@ -70,6 +70,8 @@ func Init() {
migrations.UpdateCronjobSpec, migrations.UpdateCronjobSpec,
migrations.UpdateBackupRecordPath, migrations.UpdateBackupRecordPath,
migrations.UpdateSnapshotRecords, migrations.UpdateSnapshotRecords,
migrations.UpdateWebDavConf,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View file

@ -451,3 +451,29 @@ var UpdateSnapshotRecords = &gormigrate.Migration{
return nil return nil
}, },
} }
var UpdateWebDavConf = &gormigrate.Migration{
ID: "20240205-update-webdav-conf",
Migrate: func(tx *gorm.DB) error {
var backup model.BackupAccount
_ = tx.Where("type = ?", constant.WebDAV).First(&backup).Error
if backup.ID == 0 {
return nil
}
varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil {
return err
}
delete(varMap, "addressItem")
if port, ok := varMap["port"]; ok {
varMap["address"] = fmt.Sprintf("%s:%v", varMap["address"], port)
delete(varMap, "port")
}
vars, _ := json.Marshal(varMap)
if err := tx.Model(&model.BackupAccount{}).Where("id = ?", backup.ID).Updates(map[string]interface{}{"vars": string(vars)}).Error; err != nil {
return err
}
return nil
},
}

View file

@ -6,6 +6,7 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"path"
"strings" "strings"
"github.com/studio-b12/gowebdav" "github.com/studio-b12/gowebdav"
@ -43,7 +44,7 @@ func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) {
} }
func (s webDAVClient) Upload(src, target string) (bool, error) { func (s webDAVClient) Upload(src, target string) (bool, error) {
targetFilePath := s.Bucket + "/" + target targetFilePath := path.Join(s.Bucket, target)
srcFile, err := os.Open(src) srcFile, err := os.Open(src)
if err != nil { if err != nil {
return false, err return false, err
@ -62,7 +63,7 @@ func (s webDAVClient) ListBuckets() ([]interface{}, error) {
} }
func (s webDAVClient) Download(src, target string) (bool, error) { func (s webDAVClient) Download(src, target string) (bool, error) {
srcPath := s.Bucket + "/" + src srcPath := path.Join(s.Bucket, src)
info, err := s.client.Stat(srcPath) info, err := s.client.Stat(srcPath)
if err != nil { if err != nil {
return false, err return false, err
@ -85,30 +86,30 @@ func (s webDAVClient) Download(src, target string) (bool, error) {
return true, err return true, err
} }
func (s webDAVClient) Exist(path string) (bool, error) { func (s webDAVClient) Exist(pathItem string) (bool, error) {
if _, err := s.client.Stat(s.Bucket + "/" + path); err != nil { if _, err := s.client.Stat(path.Join(s.Bucket, pathItem)); err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
func (s webDAVClient) Size(path string) (int64, error) { func (s webDAVClient) Size(pathItem string) (int64, error) {
file, err := s.client.Stat(s.Bucket + "/" + path) file, err := s.client.Stat(path.Join(s.Bucket, pathItem))
if err != nil { if err != nil {
return 0, err return 0, err
} }
return file.Size(), nil return file.Size(), nil
} }
func (s webDAVClient) Delete(filePath string) (bool, error) { func (s webDAVClient) Delete(pathItem string) (bool, error) {
if err := s.client.Remove(s.Bucket + "/" + filePath); err != nil { if err := s.client.Remove(path.Join(s.Bucket, pathItem)); err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
func (s webDAVClient) ListObjects(prefix string) ([]string, error) { func (s webDAVClient) ListObjects(prefix string) ([]string, error) {
files, err := s.client.ReadDir(s.Bucket + "/" + prefix) files, err := s.client.ReadDir(path.Join(s.Bucket, prefix))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -1250,6 +1250,7 @@ const message = {
MINIO: 'MINIO', MINIO: 'MINIO',
SFTP: 'SFTP', SFTP: 'SFTP',
WebDAV: 'WebDAV', WebDAV: 'WebDAV',
WebDAVAlist: 'WebDAV connect Alist can refer to the official documentation',
OneDrive: 'Microsoft OneDrive', OneDrive: 'Microsoft OneDrive',
isCN: 'Century Internet', isCN: 'Century Internet',
isNotCN: 'International Version', isNotCN: 'International Version',

View file

@ -1173,6 +1173,7 @@ const message = {
MINIO: 'MINIO', MINIO: 'MINIO',
SFTP: 'SFTP', SFTP: 'SFTP',
WebDAV: 'WebDAV', WebDAV: 'WebDAV',
WebDAVAlist: 'WebDAV 連接 Alist 可參考官方文檔',
OneDrive: '微軟 OneDrive', OneDrive: '微軟 OneDrive',
isCN: '世紀互聯', isCN: '世紀互聯',
isNotCN: '國際版', isNotCN: '國際版',
@ -1183,7 +1184,7 @@ const message = {
refreshTime: '令牌刷新時間', refreshTime: '令牌刷新時間',
refreshStatus: '令牌刷新狀態', refreshStatus: '令牌刷新狀態',
codeWarning: '當前授權碼格式錯誤請重新確認', codeWarning: '當前授權碼格式錯誤請重新確認',
backupDir: '備份路徑', backupDir: '備份目录',
code: '授權碼', code: '授權碼',
codeHelper: codeHelper:
'請點擊獲取按鈕然後登錄 OneDrive 復製跳轉鏈接中 code 後面的內容粘貼到該輸入框中具體操作可參考官方文檔', '請點擊獲取按鈕然後登錄 OneDrive 復製跳轉鏈接中 code 後面的內容粘貼到該輸入框中具體操作可參考官方文檔',

View file

@ -1174,6 +1174,7 @@ const message = {
MINIO: 'MINIO', MINIO: 'MINIO',
SFTP: 'SFTP', SFTP: 'SFTP',
WebDAV: 'WebDAV', WebDAV: 'WebDAV',
WebDAVAlist: 'WebDAV 连接 Alist 可参考官方文档',
OneDrive: '微软 OneDrive', OneDrive: '微软 OneDrive',
isCN: '世纪互联', isCN: '世纪互联',
isNotCN: '国际版', isNotCN: '国际版',
@ -1184,7 +1185,7 @@ const message = {
refreshTime: '令牌刷新时间', refreshTime: '令牌刷新时间',
refreshStatus: '令牌刷新状态', refreshStatus: '令牌刷新状态',
codeWarning: '当前授权码格式错误请重新确认', codeWarning: '当前授权码格式错误请重新确认',
backupDir: '备份路径', backupDir: '备份目录',
code: '授权码', code: '授权码',
codeHelper: codeHelper:
'请点击获取按钮然后登录 OneDrive 复制跳转链接中 code 后面的内容粘贴到该输入框中具体操作可参考官方文档', '请点击获取按钮然后登录 OneDrive 复制跳转链接中 code 后面的内容粘贴到该输入框中具体操作可参考官方文档',

View file

@ -372,7 +372,7 @@
<el-form-item :label="$t('commons.table.port')"> <el-form-item :label="$t('commons.table.port')">
{{ sftpData.varsJson['port'] }} {{ sftpData.varsJson['port'] }}
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.path')"> <el-form-item :label="$t('setting.backupDir')">
{{ sftpData.bucket }} {{ sftpData.bucket }}
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.table.createdAt')"> <el-form-item :label="$t('commons.table.createdAt')">
@ -408,10 +408,7 @@
<el-form-item :label="$t('setting.address')"> <el-form-item :label="$t('setting.address')">
{{ webDAVData.varsJson['address'] }} {{ webDAVData.varsJson['address'] }}
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.table.port')"> <el-form-item :label="$t('setting.backupDir')">
{{ webDAVData.varsJson['port'] }}
</el-form-item>
<el-form-item :label="$t('setting.path')">
{{ webDAVData.bucket }} {{ webDAVData.bucket }}
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.table.createdAt')"> <el-form-item :label="$t('commons.table.createdAt')">

View file

@ -39,7 +39,7 @@
v-model.trim="sftpData.rowData!.credential" v-model.trim="sftpData.rowData!.credential"
/> />
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.path')" prop="bucket" :rules="[Rules.requiredInput]"> <el-form-item :label="$t('setting.backupDir')" prop="bucket" :rules="[Rules.requiredInput]">
<el-input v-model.trim="sftpData.rowData!.bucket" /> <el-input v-model.trim="sftpData.rowData!.bucket" />
</el-form-item> </el-form-item>
</el-col> </el-col>

View file

@ -12,20 +12,21 @@
</el-form-item> </el-form-item>
<el-form-item <el-form-item
:label="$t('setting.address')" :label="$t('setting.address')"
prop="varsJson.addressItem" prop="varsJson.address"
:rules="Rules.requiredInput" :rules="Rules.requiredInput"
> >
<el-input v-model="webdavData.rowData!.varsJson['addressItem']"> <el-input v-model="webdavData.rowData!.varsJson['address']" />
<template #prepend> <span class="input-help">
<el-select v-model.trim="addressProto" style="width: 100px"> {{ $t('setting.WebDAVAlist') }}
<el-option label="http" value="http" /> <el-link
<el-option label="https" value="https" /> style="font-size: 12px; margin-left: 5px"
</el-select> icon="Position"
</template> @click="toDoc()"
</el-input> type="primary"
</el-form-item> >
<el-form-item :label="$t('commons.table.port')" prop="varsJson.port" :rules="[portRule]"> {{ $t('firewall.quickJump') }}
<el-input-number v-model.number="webdavData.rowData!.varsJson['port']" /> </el-link>
</span>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
:label="$t('commons.login.username')" :label="$t('commons.login.username')"
@ -46,7 +47,7 @@
v-model.trim="webdavData.rowData!.credential" v-model.trim="webdavData.rowData!.credential"
/> />
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.path')" prop="bucket" :rules="[Rules.requiredInput]"> <el-form-item :label="$t('setting.backupDir')" prop="bucket" :rules="[Rules.requiredInput]">
<el-input v-model.trim="webdavData.rowData!.bucket" /> <el-input v-model.trim="webdavData.rowData!.bucket" />
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -67,7 +68,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue'; import { ref } from 'vue';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElForm } from 'element-plus'; import { ElForm } from 'element-plus';
@ -75,13 +76,11 @@ import { Backup } from '@/api/interface/backup';
import DrawerHeader from '@/components/drawer-header/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue';
import { addBackup, editBackup } from '@/api/modules/setting'; import { addBackup, editBackup } from '@/api/modules/setting';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { checkPort, spliceHttp, splitHttp } from '@/utils/util';
const loading = ref(false); const loading = ref(false);
type FormInstance = InstanceType<typeof ElForm>; type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const addressProto = ref('http');
const emit = defineEmits(['search']); const emit = defineEmits(['search']);
interface DialogProps { interface DialogProps {
@ -94,24 +93,8 @@ const webdavData = ref<DialogProps>({
title: '', title: '',
}); });
const portRule = reactive({ validator: checkportRule, trigger: 'blur', type: 'number' });
function checkportRule(rule: any, value: any, callback: any) {
if (value !== undefined && value !== null) {
if (checkPort(value)) {
return callback(new Error(i18n.global.t('commons.rule.port')));
}
}
callback();
}
const acceptParams = (params: DialogProps): void => { const acceptParams = (params: DialogProps): void => {
webdavData.value = params; webdavData.value = params;
if (webdavData.value.title === 'edit') {
let httpItem = splitHttp(webdavData.value.rowData!.varsJson['address']);
webdavData.value.rowData!.varsJson['addressItem'] = httpItem.url;
addressProto.value = httpItem.proto;
}
title.value = i18n.global.t('commons.button.' + webdavData.value.title); title.value = i18n.global.t('commons.button.' + webdavData.value.title);
drawerVisible.value = true; drawerVisible.value = true;
}; };
@ -121,15 +104,15 @@ const handleClose = () => {
drawerVisible.value = false; drawerVisible.value = false;
}; };
const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/settings/', '_blank', 'noopener,noreferrer');
};
const onSubmit = async (formEl: FormInstance | undefined) => { const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
formEl.validate(async (valid) => { formEl.validate(async (valid) => {
if (!valid) return; if (!valid) return;
if (!webdavData.value.rowData) return; if (!webdavData.value.rowData) return;
webdavData.value.rowData!.varsJson['address'] = spliceHttp(
addressProto.value,
webdavData.value.rowData!.varsJson['addressItem'],
);
webdavData.value.rowData.vars = JSON.stringify(webdavData.value.rowData!.varsJson); webdavData.value.rowData.vars = JSON.stringify(webdavData.value.rowData!.varsJson);
loading.value = true; loading.value = true;
if (webdavData.value.title === 'create') { if (webdavData.value.title === 'create') {