feat: add proxy support for file wget downloads (#12942)

This commit is contained in:
2026-06-05 18:49:34 +08:00
committed by GitHub
parent e55abfb174
commit a69f7b1144
27 changed files with 139 additions and 8 deletions
+1
View File
@@ -121,6 +121,7 @@ type FileWget struct {
Path string `json:"path" validate:"required"`
Name string `json:"name" validate:"required"`
IgnoreCertificate bool `json:"ignoreCertificate"`
UseProxy bool `json:"useProxy"`
}
type FileMove struct {
+17 -1
View File
@@ -830,7 +830,23 @@ func (f *FileService) ChangeName(req request.FileRename) error {
func (f *FileService) Wget(w request.FileWget) (string, error) {
fo := files.NewFileOp()
key := "file-wget-" + common.GetUuid()
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, w.IgnoreCertificate)
options := files.DownloadOptions{
IgnoreCertificate: w.IgnoreCertificate,
}
if w.UseProxy {
systemProxy, err := NewISettingService().GetSystemProxy()
if err != nil {
return "", err
}
options.Proxy = &files.DownloadProxyConfig{
Type: systemProxy.Type,
URL: systemProxy.URL,
Port: systemProxy.Port,
User: systemProxy.User,
Password: systemProxy.Password,
}
}
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options)
}
func (f *FileService) MvFile(m request.FileMove) error {
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: 'File-management AI summary requires an enabled AI account
ErrFileAISearchBadPattern: 'Invalid file search pattern (regex or whole word): {{ .detail }}'
ErrWgetRemoteFailed: 'Remote download failed or URL is unreachable: {{ .detail }}'
ErrWgetInvalidContentType: 'Remote URL did not return a file (possibly an error page), {{ .detail }}'
ErrWgetProxyNotConfigured: 'Configure a proxy server in panel settings first'
ErrWgetProxyInvalid: 'Invalid proxy server address: {{ .detail }}'
ErrRecordExist: 'Record already exists'
ErrRecordNotFound: 'Record not found'
ErrStructTransform: 'Type conversion failed: {{ .err }}'
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: 'La búsqueda con IA de archivos está desactivada o sin cu
ErrFileAISearchBadPattern: 'Patrón de búsqueda no válido (regex u otra opción): {{ .detail }}'
ErrWgetRemoteFailed: 'La descarga remota falló o la URL no es accesible: {{ .detail }}'
ErrWgetInvalidContentType: 'La URL remota no devolvió un archivo (posiblemente una página de error), {{ .detail }}'
ErrWgetProxyNotConfigured: 'Configure primero un servidor proxy en los ajustes del panel'
ErrWgetProxyInvalid: 'Dirección del servidor proxy no válida: {{ .detail }}'
ErrRecordExist: 'Registro ya existe'
ErrRecordNotFound: 'Registro no encontrado'
ErrStructTransform: 'Error de conversión: {{ .err }}'
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: 'ファイル管理の「AI アカウント」でファイ
ErrFileAISearchBadPattern: '検索パターンが無効です(正規表現など): {{ .detail }}'
ErrWgetRemoteFailed: 'リモートのダウンロード URL が無効か、到達できません:{{ .detail }}'
ErrWgetInvalidContentType: 'リモート URL がファイルではなくエラーページ等を返しました、{{ .detail }}'
ErrWgetProxyNotConfigured: '先にパネル設定でプロキシサーバーを設定してください'
ErrWgetProxyInvalid: 'プロキシサーバーのアドレスが無効です:{{ .detail }}'
ErrRecordExist: 'レコードが既に存在します'
ErrRecordNotFound: 'レコードが見つかりません'
ErrStructTransform: '型変換エラー: {{ .err }}'
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: '파일 관리에서 「AI 계정」으로 파일 AI 검색
ErrFileAISearchBadPattern: '검색 패턴이 잘못되었습니다(정규식 등): {{ .detail }}'
ErrWgetRemoteFailed: '원격 다운로드 주소가 잘못되었거나 접근할 수 없습니다: {{ .detail }}'
ErrWgetInvalidContentType: '원격 주소가 파일이 아닌 콘텐츠를 반환했습니다 (오류 페이지일 수 있음), {{ .detail }}'
ErrWgetProxyNotConfigured: '먼저 패널 설정에서 프록시 서버를 구성하세요'
ErrWgetProxyInvalid: '프록시 서버 주소가 잘못되었습니다: {{ .detail }}'
ErrRecordExist: '레코드가 이미 있습니다'
ErrRecordNotFound: '레코드를 찾을 수 없습니다'
ErrStructTransform: '형 변환 실패: {{ .err }}'
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: 'Carian AI fail dimatikan atau tiada akaun. Dalam Pengurusa
ErrFileAISearchBadPattern: 'Corak carian tidak sah (regex dsb.): {{ .detail }}'
ErrWgetRemoteFailed: 'Muat turun jauh gagal atau URL tidak boleh diakses: {{ .detail }}'
ErrWgetInvalidContentType: 'URL jauh tidak mengembalikan fail (mungkin halaman ralat), {{ .detail }}'
ErrWgetProxyNotConfigured: 'Sila konfigurasikan pelayan proksi dalam tetapan panel dahulu'
ErrWgetProxyInvalid: 'Alamat pelayan proksi tidak sah: {{ .detail }}'
ErrRecordExist: 'Rekod sudah wujud'
ErrRecordNotFound: 'Rekod tidak ditemui'
ErrStructTransform: 'Ralat penukaran: {{ .err }}'
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: 'A pesquisa com IA de arquivos está desativada ou sem cont
ErrFileAISearchBadPattern: 'Padrão de busca inválido (regex ou opção): {{ .detail }}'
ErrWgetRemoteFailed: 'Falha no download remoto ou URL inacessível: {{ .detail }}'
ErrWgetInvalidContentType: 'A URL remota não retornou um arquivo (possivelmente uma página de erro), {{ .detail }}'
ErrWgetProxyNotConfigured: 'Configure primeiro um servidor proxy nas configurações do painel'
ErrWgetProxyInvalid: 'Endereço do servidor proxy inválido: {{ .detail }}'
ErrRecordExist: 'Registro já existe'
ErrRecordNotFound: 'Registro não encontrado'
ErrStructTransform: 'Erro de conversão: {{ .err }}'
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: 'Поиск с AI в файлах выключен или
ErrFileAISearchBadPattern: 'Недопустимый шаблон поиска (regex и т.п.): {{ .detail }}'
ErrWgetRemoteFailed: 'Неверный или недоступный URL удалённой загрузки: {{ .detail }}'
ErrWgetInvalidContentType: 'Удалённый сервер вернул не файл (возможно, страницу ошибки), {{ .detail }}'
ErrWgetProxyNotConfigured: 'Сначала настройте прокси-сервер в настройках панели'
ErrWgetProxyInvalid: 'Недопустимый адрес прокси-сервера: {{ .detail }}'
ErrRecordExist: 'Запись уже существует'
ErrRecordNotFound: 'Запись не найдена'
ErrStructTransform: 'Ошибка преобразования: {{ .err }}'
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: 'Dosya yönetimi AI araması kapalı veya hesap yok. Dosya
ErrFileAISearchBadPattern: 'Geçersiz arama deseni (regex vb.): {{ .detail }}'
ErrWgetRemoteFailed: 'Uzak indirme başarısız veya URL erişilemiyor: {{ .detail }}'
ErrWgetInvalidContentType: 'Uzak URL bir dosya değil, muhtemelen bir hata sayfası döndürdü, {{ .detail }}'
ErrWgetProxyNotConfigured: 'Önce panel ayarlarında bir proxy sunucusu yapılandırın'
ErrWgetProxyInvalid: 'Proxy sunucusu adresi geçersiz: {{ .detail }}'
ErrRecordExist: 'Kayıt zaten var'
ErrRecordNotFound: 'Kayıt bulunamadı'
ErrStructTransform: 'Dönüşüm hatası: {{ .err }}'
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: '請先在檔案管理頁透過「AI 帳號」啟用檔案
ErrFileAISearchBadPattern: '搜尋條件無效(正則或全詞): {{ .detail }}'
ErrWgetRemoteFailed: '遠端下載網址無效或無法存取:{{ .detail }}'
ErrWgetInvalidContentType: '遠端網址未回傳檔案內容(可能是錯誤頁),{{ .detail }}'
ErrWgetProxyNotConfigured: '請先在面板設定中設定代理伺服器'
ErrWgetProxyInvalid: '代理伺服器位址無效:{{ .detail }}'
ErrRecordExist: '記錄已存在'
ErrRecordNotFound: '記錄未能找到'
ErrStructTransform: '型別轉換失敗: {{ .err }}'
+2
View File
@@ -6,6 +6,8 @@ ErrFileAINotEnabled: "生成 AI 总结需在文件管理中启用并保存文件
ErrFileAISearchBadPattern: "搜索条件无效(正则或全词匹配): {{ .detail }}"
ErrWgetRemoteFailed: "远程下载地址无效或无法访问:{{ .detail }}"
ErrWgetInvalidContentType: "远程地址返回的不是文件内容(可能是错误页),{{ .detail }}"
ErrWgetProxyNotConfigured: "请先在面板设置中配置代理服务器"
ErrWgetProxyInvalid: "代理服务器地址无效:{{ .detail }}"
ErrRecordExist: "记录已存在"
ErrRecordNotFound: "记录不存在"
ErrStructTransform: "类型转换失败: {{ .err }}"
+70 -5
View File
@@ -10,7 +10,9 @@ import (
"fmt"
"io"
"io/fs"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path"
@@ -365,6 +367,19 @@ type Process struct {
Name string `json:"name"`
}
type DownloadProxyConfig struct {
Type string
URL string
Port string
User string
Password string
}
type DownloadOptions struct {
IgnoreCertificate bool
Proxy *DownloadProxyConfig
}
func (w *WriteCounter) Write(p []byte) (n int, err error) {
n = len(p)
w.Written += uint64(n)
@@ -392,12 +407,62 @@ func (w *WriteCounter) SaveProcess() {
}
}
func (f FileOp) DownloadFileWithProcess(url, dst, key string, ignoreCertificate bool) error {
client := &http.Client{}
if ignoreCertificate {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
func buildDownloadProxyURL(proxy DownloadProxyConfig) (*url.URL, error) {
proxyType := strings.TrimSpace(proxy.Type)
proxyHost := strings.TrimSpace(proxy.URL)
if proxyType == "" || proxyHost == "" {
return nil, buserr.New("ErrWgetProxyNotConfigured")
}
if !strings.Contains(proxyHost, "://") {
proxyHost = fmt.Sprintf("%s://%s", proxyType, proxyHost)
}
parsedURL, err := url.Parse(proxyHost)
if err != nil {
return nil, buserr.WithDetail("ErrWgetProxyInvalid", err.Error(), err)
}
if parsedURL.Scheme == "" {
parsedURL.Scheme = proxyType
}
if parsedURL.Host == "" && parsedURL.Path != "" {
parsedURL.Host = parsedURL.Path
parsedURL.Path = ""
}
if parsedURL.Host == "" {
return nil, buserr.New("ErrWgetProxyNotConfigured")
}
if strings.TrimSpace(proxy.Port) != "" && parsedURL.Port() == "" {
parsedURL.Host = net.JoinHostPort(parsedURL.Hostname(), strings.TrimSpace(proxy.Port))
}
if proxy.User != "" && proxy.Password != "" {
parsedURL.User = url.UserPassword(proxy.User, proxy.Password)
} else if proxy.User != "" {
parsedURL.User = url.User(proxy.User)
}
return parsedURL, nil
}
func newDownloadHTTPClient(options DownloadOptions) (*http.Client, error) {
if !options.IgnoreCertificate && options.Proxy == nil {
return &http.Client{}, nil
}
transport := &http.Transport{}
if options.IgnoreCertificate {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if options.Proxy != nil {
proxyURL, err := buildDownloadProxyURL(*options.Proxy)
if err != nil {
return nil, err
}
transport.Proxy = http.ProxyURL(proxyURL)
}
return &http.Client{Transport: transport}, nil
}
func (f FileOp) DownloadFileWithProcess(url, dst, key string, options DownloadOptions) error {
client, err := newDownloadHTTPClient(options)
if err != nil {
return err
}
defer client.CloseIdleConnections()
+1
View File
@@ -212,6 +212,7 @@ export namespace File {
name: string;
url: string;
ignoreCertificate?: boolean;
useProxy?: boolean;
}
export interface FileWgetRes {
+2
View File
@@ -2302,6 +2302,8 @@ const message = {
ignoreCertificate: 'Allow insecure server connections',
ignoreCertificateHelper:
'Allowing inscure server connections may lead to data leak or tampering. Use this option only when trusting the download source.',
useProxy: 'Use proxy for download',
useProxyHelper: 'Use the proxy server configured in panel settings to access the remote download URL',
uploadOverLimit: 'The number of files exceeds 1000! Please compress and upload',
clashDidNotSupport: 'File names are prohibited from containing .1panel_clash',
clashDeleteAlert: 'The "Recycle Bin" folder can\'t be deleted',
+2
View File
@@ -2359,6 +2359,8 @@ const message = {
ignoreCertificate: 'Permitir conexiones inseguras',
ignoreCertificateHelper:
'Permitir conexiones inseguras puede provocar fugas o alteraciones de datos. Úselo solo si confía en la fuente.',
useProxy: 'Descargar mediante proxy',
useProxyHelper: 'Usar el servidor proxy configurado en el panel para acceder a la URL remota',
uploadOverLimit: '¡El número de archivos supera los 1000! Por favor, comprima y cargue',
clashDidNotSupport: 'Los nombres de archivo no pueden contener .1panel_clash',
clashDeleteAlert: 'No se puede eliminar la carpeta "Papelera"',
+2
View File
@@ -2326,6 +2326,8 @@ const message = {
ignoreCertificate: '不安定なサーバー接続を許可します',
ignoreCertificateHelper:
'不安定なサーバー接続を可能にするとデータが漏れたり改ざんしたりする可能性がありますダウンロードソースを信頼する場合にのみこのオプションを使用します',
useProxy: 'プロキシでダウンロード',
useProxyHelper: 'パネル設定のプロキシサーバーを使用してリモート URL にアクセスします',
uploadOverLimit: 'ファイルの数は1000を超えています圧縮してアップロードしてください',
clashDidNotSupport: 'ファイル名は.1panel_clashを含むことを禁止されています',
clashDeleteAlert: 'リサイクルビンフォルダーを削除することはできません',
+2
View File
@@ -2284,6 +2284,8 @@ const message = {
ignoreCertificate: '안전하지 않은 서버 연결 허용',
ignoreCertificateHelper:
'안전하지 않은 서버 연결을 허용하면 데이터 유출 또는 변조가 발생할 있습니다. 옵션은 다운로드 소스를 신뢰할 때만 사용하세요.',
useProxy: '프록시로 다운로드',
useProxyHelper: '패널 설정의 프록시 서버로 원격 다운로드 주소에 접근합니다',
uploadOverLimit: '파일 수가 1000 개를 초과했습니다! 압축하여 업로드하세요.',
clashDidNotSupport: '파일 이름에 .1panel_clash 포함할 없습니다.',
clashDeleteAlert: '"휴지통" 폴더는 삭제할 없습니다.',
+2
View File
@@ -2363,6 +2363,8 @@ const message = {
ignoreCertificate: 'Benarkan sambungan pelayan tidak selamat',
ignoreCertificateHelper:
'Membenarkan sambungan pelayan tidak selamat boleh menyebabkan kebocoran atau pengubahan data. Gunakan pilihan ini hanya jika mempercayai sumber muat turun.',
useProxy: 'Muat turun melalui proksi',
useProxyHelper: 'Gunakan pelayan proksi dalam tetapan panel untuk mengakses URL muat turun jauh',
uploadOverLimit: 'Bilangan fail melebihi 1000! Sila mampatkan dan muat naik',
clashDidNotSupport: 'Nama fail dilarang mengandungi .1panel_clash',
clashDeleteAlert: 'Folder "Tong Kitar Semula" tidak boleh dipadamkan',
+2
View File
@@ -2476,6 +2476,8 @@ const message = {
ignoreCertificate: 'Permitir conexões inseguras com o servidor',
ignoreCertificateHelper:
'Permitir conexões inseguras com o servidor pode levar a vazamento ou adulteração de dados. Use esta opção apenas quando confiar na fonte de download.',
useProxy: 'Baixar via proxy',
useProxyHelper: 'Usar o servidor proxy configurado no painel para acessar a URL remota',
uploadOverLimit: 'O número de arquivos excede 1000! Por favor, compacte e envie novamente',
clashDidNotSupport: 'Os nomes de arquivos são proibidos de conter .1panel_clash',
clashDeleteAlert: 'A pasta "Lixeira" não pode ser excluída',
+2
View File
@@ -2343,6 +2343,8 @@ const message = {
ignoreCertificate: 'Разрешить небезопасные подключения к серверу',
ignoreCertificateHelper:
'Разрешение небезопасных подключений к серверу может привести к утечке или подмене данных. Используйте эту опцию только если доверяете источнику загрузки.',
useProxy: 'Загружать через прокси',
useProxyHelper: 'Использовать прокси-сервер из настроек панели для доступа к удалённому URL загрузки',
uploadOverLimit: 'Количество файлов превышает 1000! Пожалуйста, сожмите и загрузите',
clashDidNotSupport: 'Имена файлов не должны содержать .1panel_clash',
clashDeleteAlert: 'Папку "Корзина" нельзя удалить',
+2
View File
@@ -2352,6 +2352,8 @@ const message = {
ignoreCertificate: 'Güvensiz sunucu bağlantılarına izin ver',
ignoreCertificateHelper:
'Güvensiz sunucu bağlantılarına izin vermek, veri sızıntısına veya değiştirilmesine yol açabilir. Bu seçeneği yalnızca indirme kaynağına güvendiğinizde kullanın.',
useProxy: 'Proxy ile indir',
useProxyHelper: 'Uzak indirme URLsine erişmek için panel ayarlarındaki proxy sunucusunu kullanır',
uploadOverLimit: 'Dosya sayısı 1000i aşıyor! Lütfen sıkıştırıp yükleyin',
clashDidNotSupport: 'Dosya adlarının .1panel_clash içermesi yasaktır',
clashDeleteAlert: '"Geri Dönüşüm Kutusu" klasörü silinemez',
+2
View File
@@ -2191,6 +2191,8 @@ const message = {
ignoreCertificate: '忽略不可信憑證',
ignoreCertificateHelper:
'下載時忽略不可信憑證可能導致資料洩露或篡改請謹慎使用此選項僅在信任下載源的情況下啟用',
useProxy: '使用代理下載',
useProxyHelper: '使用面板設定中的代理伺服器存取遠端下載網址',
uploadOverLimit: '檔案數量超過 1000請壓縮後上傳',
clashDidNotSupport: '檔名禁止包含 .1panel_clash',
clashDeleteAlert: '回收站資料夾不能刪除',
+2
View File
@@ -2171,6 +2171,8 @@ const message = {
ignoreCertificate: '忽略不可信证书',
ignoreCertificateHelper:
'下载时忽略不可信证书可能导致数据泄露或篡改请谨慎使用此选项仅在信任下载源的情况下启用',
useProxy: '使用代理下载',
useProxyHelper: '使用面板设置中的代理服务器访问远程下载地址',
uploadOverLimit: '文件数量超过 1000请压缩后上传',
clashDidNotSupport: '文件名禁止包含 .1panel_clash',
clashDeleteAlert: '回收站文件夹不能删除',
@@ -7,7 +7,7 @@
>
<div class="flex items-center justify-between gap-3">
<div class="space-y-1">
<div class="text-sm font-medium">
<div class="text-sm font-medium text-[var(--el-text-color-secondary)]">
{{ $t('commons.status.executing') }}
</div>
<div class="text-xs text-[var(--el-text-color-secondary)]">
@@ -7,7 +7,7 @@
>
<div class="flex items-center justify-between gap-3">
<div class="space-y-1">
<div class="text-sm font-medium">
<div class="text-sm font-medium text-[var(--el-text-color-secondary)]">
{{ $t('commons.status.executing') }}
</div>
<div class="text-xs text-[var(--el-text-color-secondary)]">
@@ -21,6 +21,12 @@
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="addForm.name"></el-input>
</el-form-item>
<el-form-item>
<el-checkbox v-model="addForm.useProxy">
{{ $t('file.useProxy') }}
</el-checkbox>
<span class="input-help">{{ $t('file.useProxyHelper') }}</span>
</el-form-item>
<el-form-item>
<el-checkbox v-model="addForm.ignoreCertificate">
{{ $t('file.ignoreCertificate') }}
@@ -88,6 +94,7 @@ const addForm = reactive({
path: '',
name: '',
ignoreCertificate: false,
useProxy: false,
});
const em = defineEmits(['close']);
@@ -136,6 +143,7 @@ const acceptParams = (props: WgetProps) => {
open.value = true;
submitData.value = false;
addForm.ignoreCertificate = false;
addForm.useProxy = false;
};
defineExpose({ acceptParams });