Skip to content

Commit e8f5d79

Browse files
committed
feat(url_tree): support custom download params with magic variables
Add a `download_params` option to the UrlTree driver so users can append custom query parameters (e.g. attname={{.Name}}) to the redirect URL on download while keeping preview links clean. Supports magic variables {{.Name}} {{.Path}} {{.Size}} {{.Modified}}, which are URL-encoded automatically. Closes #9568.
1 parent d0cec67 commit e8f5d79

3 files changed

Lines changed: 92 additions & 6 deletions

File tree

drivers/url_tree/driver.go

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package url_tree
33
import (
44
"context"
55
"errors"
6+
"fmt"
7+
"net/url"
68
stdpath "path"
79
"strings"
810
"sync"
11+
"text/template"
912

1013
"github.com/alist-org/alist/v3/internal/driver"
1114
"github.com/alist-org/alist/v3/internal/errs"
@@ -18,8 +21,9 @@ import (
1821
type Urls struct {
1922
model.Storage
2023
Addition
21-
root *Node
22-
mutex sync.RWMutex
24+
root *Node
25+
mutex sync.RWMutex
26+
downloadParamsTmpl *template.Template
2327
}
2428

2529
func (d *Urls) Config() driver.Config {
@@ -37,6 +41,14 @@ func (d *Urls) Init(ctx context.Context) error {
3741
}
3842
node.calSize()
3943
d.root = node
44+
d.downloadParamsTmpl = nil
45+
if params := strings.TrimSpace(d.DownloadParams); params != "" {
46+
tmpl, err := template.New("downloadParams").Parse(params)
47+
if err != nil {
48+
return fmt.Errorf("failed to parse download_params: %w", err)
49+
}
50+
d.downloadParamsTmpl = tmpl
51+
}
4052
return nil
4153
}
4254

@@ -76,13 +88,49 @@ func (d *Urls) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
7688
return nil, errs.ObjectNotFound
7789
}
7890
if node.isFile() {
91+
downURL := node.Url
92+
// Append custom download parameters on download, but keep preview links clean.
93+
if args.Type != "preview" && d.downloadParamsTmpl != nil {
94+
var err error
95+
downURL, err = d.buildDownloadURL(node, file.GetPath())
96+
if err != nil {
97+
return nil, err
98+
}
99+
}
79100
return &model.Link{
80-
URL: node.Url,
101+
URL: downURL,
81102
}, nil
82103
}
83104
return nil, errs.NotFile
84105
}
85106

107+
// buildDownloadURL renders the configured download_params template with the
108+
// node's magic variables and appends the result as query parameters to the URL.
109+
func (d *Urls) buildDownloadURL(node *Node, path string) (string, error) {
110+
var sb strings.Builder
111+
err := d.downloadParamsTmpl.Execute(&sb, map[string]interface{}{
112+
"Name": node.Name,
113+
"Path": path,
114+
"Size": node.Size,
115+
"Modified": node.Modified,
116+
})
117+
if err != nil {
118+
return "", fmt.Errorf("failed to render download_params: %w", err)
119+
}
120+
rendered := strings.TrimSpace(sb.String())
121+
rendered = strings.TrimPrefix(rendered, "?")
122+
if rendered == "" {
123+
return node.Url, nil
124+
}
125+
// Parse the rendered string so that magic-variable values (e.g. file names
126+
// with non-ASCII characters) are properly URL-encoded by Encode().
127+
query, err := url.ParseQuery(rendered)
128+
if err != nil {
129+
return "", fmt.Errorf("invalid download_params %q: %w", rendered, err)
130+
}
131+
return utils.InjectQuery(node.Url, query)
132+
}
133+
86134
func (d *Urls) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
87135
if !d.Writable {
88136
return nil, errs.PermissionDenied

drivers/url_tree/meta.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ type Addition struct {
1010
// driver.RootPath
1111
// driver.RootID
1212
// define other
13-
UrlStructure string `json:"url_structure" type:"text" required:"true" default:"https://jsd.nn.ci/gh/alist-org/alist/README.md\nhttps://jsd.nn.ci/gh/alist-org/alist/README_cn.md\nfolder:\n CONTRIBUTING.md:1635:https://jsd.nn.ci/gh/alist-org/alist/CONTRIBUTING.md\n CODE_OF_CONDUCT.md:2093:https://jsd.nn.ci/gh/alist-org/alist/CODE_OF_CONDUCT.md" help:"structure:FolderName:\n [FileName:][FileSize:][Modified:]Url"`
14-
HeadSize bool `json:"head_size" type:"bool" default:"false" help:"Use head method to get file size, but it may be failed."`
15-
Writable bool `json:"writable" type:"bool" default:"false"`
13+
UrlStructure string `json:"url_structure" type:"text" required:"true" default:"https://jsd.nn.ci/gh/alist-org/alist/README.md\nhttps://jsd.nn.ci/gh/alist-org/alist/README_cn.md\nfolder:\n CONTRIBUTING.md:1635:https://jsd.nn.ci/gh/alist-org/alist/CONTRIBUTING.md\n CODE_OF_CONDUCT.md:2093:https://jsd.nn.ci/gh/alist-org/alist/CODE_OF_CONDUCT.md" help:"structure:FolderName:\n [FileName:][FileSize:][Modified:]Url"`
14+
HeadSize bool `json:"head_size" type:"bool" default:"false" help:"Use head method to get file size, but it may be failed."`
15+
Writable bool `json:"writable" type:"bool" default:"false"`
16+
DownloadParams string `json:"download_params" type:"text" help:"Extra query parameters appended to the URL on download (not on preview). Magic variables: {{.Name}} {{.Path}} {{.Size}} {{.Modified}}; values are URL-encoded automatically. e.g. attname={{.Name}}"`
1617
}
1718

1819
var config = driver.Config{

drivers/url_tree/urls_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package url_tree_test
22

33
import (
4+
"context"
45
"testing"
56

67
"github.com/alist-org/alist/v3/drivers/url_tree"
8+
"github.com/alist-org/alist/v3/internal/model"
79
)
810

911
func testTree() (*url_tree.Node, error) {
@@ -45,3 +47,38 @@ func TestGetNode(t *testing.T) {
4547
t.Errorf("got wrong node: %+v", url3)
4648
}
4749
}
50+
51+
func TestDownloadParams(t *testing.T) {
52+
d := &url_tree.Urls{
53+
Addition: url_tree.Addition{
54+
UrlStructure: "测试文件.mp4:https://cdn.example.com/random123",
55+
DownloadParams: "attname={{.Name}}",
56+
},
57+
}
58+
if err := d.Init(context.Background()); err != nil {
59+
t.Fatalf("failed to init: %+v", err)
60+
}
61+
file, err := d.Get(context.Background(), "/测试文件.mp4")
62+
if err != nil {
63+
t.Fatalf("failed to get file: %+v", err)
64+
}
65+
66+
// preview must keep the original (clean) URL
67+
previewLink, err := d.Link(context.Background(), file, model.LinkArgs{Type: "preview"})
68+
if err != nil {
69+
t.Fatalf("failed to get preview link: %+v", err)
70+
}
71+
if previewLink.URL != "https://cdn.example.com/random123" {
72+
t.Errorf("preview link should be clean, got: %s", previewLink.URL)
73+
}
74+
75+
// download must append the encoded custom parameters
76+
downLink, err := d.Link(context.Background(), file, model.LinkArgs{})
77+
if err != nil {
78+
t.Fatalf("failed to get download link: %+v", err)
79+
}
80+
want := "https://cdn.example.com/random123?attname=%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.mp4"
81+
if downLink.URL != want {
82+
t.Errorf("download link mismatch:\n got: %s\nwant: %s", downLink.URL, want)
83+
}
84+
}

0 commit comments

Comments
 (0)