feat(workflow): HTTP auth and custom file name for remote downloads (#3072)
- DownloadWorkflowService accepts optional file_name/username/password - Per-task options persisted in RemoteDownloadTaskState for resumability - aria2: mapped to out/http-user/http-passwd; qBittorrent: rename plus URL userinfo credentials - File name sanitized against path separators; creds only applied to plain HTTP(S) sources - New fields in the remote download dialog (en-US, zh-CN) Authored By: TDvorak <info@tdvorak.dev> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>pull/3582/head
parent
24c751961e
commit
d165acae77
@ -0,0 +1,120 @@
|
||||
package workflows
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type stubNode struct {
|
||||
cluster.Node
|
||||
settings *types.NodeSetting
|
||||
}
|
||||
|
||||
func (s *stubNode) Settings(ctx context.Context) *types.NodeSetting {
|
||||
return s.settings
|
||||
}
|
||||
|
||||
func newRemoteDownloadTaskForOptions(state *RemoteDownloadTaskState, provider types.DownloaderProvider) *RemoteDownloadTask {
|
||||
return &RemoteDownloadTask{
|
||||
state: state,
|
||||
node: &stubNode{settings: &types.NodeSetting{Provider: provider}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDownloadOptionsAria2(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
m := newRemoteDownloadTaskForOptions(&RemoteDownloadTaskState{
|
||||
SrcUri: "https://example.com/file.zip",
|
||||
FileName: "renamed.zip",
|
||||
HTTPUsername: "user",
|
||||
HTTPPassword: "pass",
|
||||
}, types.DownloaderProviderAria2)
|
||||
|
||||
base := map[string]interface{}{"max-connection-per-server": "4"}
|
||||
opts, taskUrl := m.buildDownloadOptions(context.Background(), base, "https://example.com/file.zip")
|
||||
|
||||
a.Equal("renamed.zip", opts["out"])
|
||||
a.Equal("user", opts["http-user"])
|
||||
a.Equal("pass", opts["http-passwd"])
|
||||
a.Equal("4", opts["max-connection-per-server"])
|
||||
a.Equal("https://example.com/file.zip", taskUrl)
|
||||
_, exists := base["out"]
|
||||
a.False(exists, "base options must not be mutated")
|
||||
}
|
||||
|
||||
func TestBuildDownloadOptionsQBittorrent(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
m := newRemoteDownloadTaskForOptions(&RemoteDownloadTaskState{
|
||||
SrcUri: "https://example.com/file.torrent",
|
||||
FileName: "renamed",
|
||||
HTTPUsername: "user",
|
||||
HTTPPassword: "p@ss:word",
|
||||
}, types.DownloaderProviderQBittorrent)
|
||||
|
||||
opts, taskUrl := m.buildDownloadOptions(context.Background(), nil, "https://example.com/file.torrent")
|
||||
|
||||
a.Equal("renamed", opts["rename"])
|
||||
_, exists := opts["out"]
|
||||
a.False(exists)
|
||||
a.Equal("https://user:p%40ss%3Aword@example.com/file.torrent", taskUrl)
|
||||
}
|
||||
|
||||
func TestBuildDownloadOptionsNonHttpSrc(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
|
||||
// Torrent source file: aria2 "out" and HTTP credentials must not apply.
|
||||
m := newRemoteDownloadTaskForOptions(&RemoteDownloadTaskState{
|
||||
SrcFileUri: "cloudreve://my/file.torrent",
|
||||
FileName: "renamed",
|
||||
HTTPUsername: "user",
|
||||
HTTPPassword: "pass",
|
||||
}, types.DownloaderProviderAria2)
|
||||
|
||||
opts, taskUrl := m.buildDownloadOptions(context.Background(), nil, "https://slave.example.com/entity/1")
|
||||
_, hasOut := opts["out"]
|
||||
a.False(hasOut)
|
||||
_, hasUser := opts["http-user"]
|
||||
a.False(hasUser)
|
||||
a.Equal("https://slave.example.com/entity/1", taskUrl)
|
||||
|
||||
// Magnet link: same for aria2.
|
||||
m = newRemoteDownloadTaskForOptions(&RemoteDownloadTaskState{
|
||||
SrcUri: "magnet:?xt=urn:btih:abc",
|
||||
FileName: "renamed",
|
||||
HTTPUsername: "user",
|
||||
}, types.DownloaderProviderAria2)
|
||||
opts, _ = m.buildDownloadOptions(context.Background(), nil, "magnet:?xt=urn:btih:abc")
|
||||
_, hasOut = opts["out"]
|
||||
a.False(hasOut)
|
||||
_, hasUser = opts["http-user"]
|
||||
a.False(hasUser)
|
||||
}
|
||||
|
||||
func TestBuildDownloadOptionsNoExtras(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
m := newRemoteDownloadTaskForOptions(&RemoteDownloadTaskState{
|
||||
SrcUri: "https://example.com/file.zip",
|
||||
}, types.DownloaderProviderAria2)
|
||||
|
||||
base := map[string]interface{}{"k": "v"}
|
||||
opts, taskUrl := m.buildDownloadOptions(context.Background(), base, "https://example.com/file.zip")
|
||||
a.Equal("v", opts["k"])
|
||||
a.Equal("https://example.com/file.zip", taskUrl)
|
||||
}
|
||||
|
||||
func TestNewRemoteDownloadTaskSanitizesFileName(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
tsk, err := NewRemoteDownloadTask(context.Background(), "https://example.com/f", "", "cloudreve://my/dst", &RemoteDownloadTaskOption{
|
||||
FileName: "../../etc/passwd",
|
||||
})
|
||||
a.NoError(err)
|
||||
|
||||
state := &RemoteDownloadTaskState{}
|
||||
a.NoError(json.Unmarshal([]byte(tsk.(*RemoteDownloadTask).Task.PrivateState), state))
|
||||
a.Equal(".._.._etc_passwd", state.FileName)
|
||||
}
|
||||
Loading…
Reference in new issue