@@ -2,6 +2,8 @@ package backup
22
33import (
44 "fmt"
5+ "net"
6+ "net/url"
57 "path"
68 "strings"
79
@@ -52,10 +54,137 @@ func newSnapshotTarget(milvusCfg, backupCfg storage.Config, backupDir string) (s
5254// the backup directory, which is what the backup meta records: an absolute uri would pin
5355// the backup to the bucket and prefix it was written to.
5456func (t snapshotTarget ) metadataPath (metadataURI string ) (string , error ) {
55- root := strings .TrimSuffix (t .Path , "/" ) + "/"
56- if ! strings .HasPrefix (metadataURI , root ) {
57+ target , err := parseSnapshotURI (t .Path )
58+ if err != nil {
59+ return "" , fmt .Errorf ("backup: parse snapshot target %s: %w" , t .Path , err )
60+ }
61+ metadata , err := parseSnapshotURI (metadataURI )
62+ if err != nil {
63+ return "" , fmt .Errorf ("backup: parse exported metadata %s: %w" , metadataURI , err )
64+ }
65+
66+ if ! target .sameStorage (metadata ) {
5767 return "" , fmt .Errorf ("backup: exported metadata %s is not under the target %s" , metadataURI , t .Path )
5868 }
5969
60- return path .Join (t .Dir , strings .TrimPrefix (metadataURI , root )), nil
70+ root := strings .TrimSuffix (target .key , "/" ) + "/"
71+ if ! strings .HasPrefix (metadata .key , root ) {
72+ return "" , fmt .Errorf ("backup: exported metadata %s is not under the target %s" , metadataURI , t .Path )
73+ }
74+
75+ return path .Join (t .Dir , strings .TrimPrefix (metadata .key , root )), nil
76+ }
77+
78+ // snapshotURI is the storage identity and object key encoded by one of the URI
79+ // forms Milvus accepts. Endpoint-style forms put the bucket in the first path
80+ // segment, while provider-style forms put it in the host.
81+ type snapshotURI struct {
82+ scheme string
83+ endpoint string
84+ bucket string
85+ key string
86+ endpointStyle bool
87+ }
88+
89+ func parseSnapshotURI (raw string ) (snapshotURI , error ) {
90+ u , err := url .Parse (raw )
91+ if err != nil {
92+ return snapshotURI {}, err
93+ }
94+ if u .Scheme == "" || u .Host == "" {
95+ return snapshotURI {}, fmt .Errorf ("uri needs a scheme and host" )
96+ }
97+ if u .User != nil || u .RawQuery != "" || u .ForceQuery || u .Fragment != "" {
98+ return snapshotURI {}, fmt .Errorf ("uri must not contain credentials, query parameters, or a fragment" )
99+ }
100+
101+ objectPath , err := url .PathUnescape (u .EscapedPath ())
102+ if err != nil {
103+ return snapshotURI {}, fmt .Errorf ("unescape object path: %w" , err )
104+ }
105+ objectPath , err = cleanSnapshotObjectKey (objectPath )
106+ if err != nil {
107+ return snapshotURI {}, err
108+ }
109+
110+ scheme := strings .ToLower (u .Scheme )
111+ switch scheme {
112+ case "minio" , "http" , "https" , "az" , "azure" :
113+ parts := strings .SplitN (objectPath , "/" , 2 )
114+ if len (parts ) != 2 || parts [0 ] == "" || parts [1 ] == "" {
115+ return snapshotURI {}, fmt .Errorf ("endpoint-style uri needs a bucket and object key" )
116+ }
117+ endpoint , err := snapshotEndpoint (u , scheme )
118+ if err != nil {
119+ return snapshotURI {}, err
120+ }
121+ return snapshotURI {
122+ scheme : scheme ,
123+ endpoint : endpoint ,
124+ bucket : parts [0 ],
125+ key : parts [1 ],
126+ endpointStyle : true ,
127+ }, nil
128+ case "s3" , "gs" , "gcs" :
129+ if u .Port () != "" {
130+ return snapshotURI {}, fmt .Errorf ("provider-style uri bucket must not contain a port" )
131+ }
132+ return snapshotURI {
133+ scheme : scheme ,
134+ bucket : u .Host ,
135+ key : objectPath ,
136+ }, nil
137+ default :
138+ return snapshotURI {}, fmt .Errorf ("unsupported snapshot uri scheme %q" , scheme )
139+ }
140+ }
141+
142+ func cleanSnapshotObjectKey (objectPath string ) (string , error ) {
143+ objectPath = strings .Trim (objectPath , "/" )
144+ if objectPath == "" {
145+ return "" , fmt .Errorf ("uri needs an object key" )
146+ }
147+ for _ , part := range strings .Split (objectPath , "/" ) {
148+ if part == "." || part == ".." {
149+ return "" , fmt .Errorf ("object key must not contain path traversal" )
150+ }
151+ }
152+ return path .Clean (objectPath ), nil
153+ }
154+
155+ func snapshotEndpoint (u * url.URL , scheme string ) (string , error ) {
156+ host := strings .ToLower (strings .TrimSuffix (u .Hostname (), "." ))
157+ if host == "" {
158+ return "" , fmt .Errorf ("uri needs an endpoint host" )
159+ }
160+ port := u .Port ()
161+ if port == "" {
162+ switch scheme {
163+ case "https" :
164+ port = "443"
165+ case "http" :
166+ port = "80"
167+ }
168+ }
169+ if port == "" {
170+ return host , nil
171+ }
172+ return net .JoinHostPort (host , port ), nil
173+ }
174+
175+ func (u snapshotURI ) sameStorage (other snapshotURI ) bool {
176+ if u .endpointStyle != other .endpointStyle || u .bucket != other .bucket {
177+ return false
178+ }
179+ if u .endpointStyle {
180+ return u .endpoint == other .endpoint
181+ }
182+ return canonicalSnapshotScheme (u .scheme ) == canonicalSnapshotScheme (other .scheme )
183+ }
184+
185+ func canonicalSnapshotScheme (scheme string ) string {
186+ if scheme == "gcs" {
187+ return "gs"
188+ }
189+ return scheme
61190}
0 commit comments