Skip to content

Commit 707693b

Browse files
committed
Start adding support fr remote templates
1 parent d2f5c70 commit 707693b

3 files changed

Lines changed: 176 additions & 0 deletions

File tree

command.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,41 @@
11
package main
22

33
import (
4+
"crypto/tls"
45
"fmt"
56
"html/template"
67
"io"
78
"maps"
9+
"net/http"
10+
"net/url"
811
"os"
912
"path"
1013
"path/filepath"
14+
"strings"
15+
"time"
1116

1217
"github.com/Masterminds/sprig/v3"
1318
"github.com/dihedron/rawdata"
1419
"github.com/dihedron/template/extensions"
20+
"github.com/jlaffaye/ftp"
1521
"golang.org/x/exp/slog"
1622
)
1723

24+
// Command is the main command for the application.
1825
type Command struct {
1926
Input *Input `short:"i" long:"input" description:"The input data, either as an inline JSON value or as a @file (in JSON or YAML format)." optional:"yes" env:"TEMPLATE_INPUT"`
2027
Templates []string `short:"t" long:"template" description:"The paths of all the templates and subtemplates on disk; the main template must be the first." required:"yes"`
2128
Output string `short:"o" long:"output" description:"The path to the output file." optional:"yes" env:"TEMPLATE_OUTPUT"`
2229
}
2330

31+
// Input is the input data for the template.
2432
type Input struct {
2533
Data any
2634
}
2735

36+
// UnmarshalFlag unmarshals the input data from a string;
37+
// if the string starts with '@', it is treated as a file path,
38+
// otherwise it is treated as an inline JSON value.
2839
func (i *Input) UnmarshalFlag(value string) error {
2940
var err error
3041
i.Data, err = rawdata.Unmarshal(value)
@@ -34,6 +45,7 @@ func (i *Input) UnmarshalFlag(value string) error {
3445
return err
3546
}
3647

48+
// Execute executes the command.
3749
func (cmd *Command) Execute(args []string) error {
3850
var err error
3951

@@ -67,12 +79,35 @@ func (cmd *Command) Execute(args []string) error {
6779
} else {
6880
output = os.Stdout
6981
}
82+
// ensure the output is closed
83+
if output, ok := output.(io.WriteCloser); ok {
84+
defer output.Close()
85+
}
7086

7187
// populate the functions map
7288
functions := template.FuncMap{}
7389
maps.Copy(functions, extensions.FuncMap())
7490
maps.Copy(functions, sprig.FuncMap())
7591

92+
// // download the templates if needed
93+
// for _, t := range cmd.Templates {
94+
// if strings.HasPrefix(t, "http://") || strings.HasPrefix(t, "https://") {
95+
// data, err := downloadHTTP(t)
96+
// if err != nil {
97+
// slog.Error("cannot download template", "error", err)
98+
// return fmt.Errorf("error downloading template %s: %w", t, err)
99+
// }
100+
// cmd.Templates = append(cmd.Templates, string(data))
101+
// } else if strings.HasPrefix(t, "ftp://") || strings.HasPrefix(t, "ftps://") || strings.HasPrefix(t, "sftp://") {
102+
// data, err := downloadFTP(t)
103+
// if err != nil {
104+
// slog.Error("cannot download template", "error", err)
105+
// return fmt.Errorf("error downloading template %s: %w", t, err)
106+
// }
107+
// cmd.Templates = append(cmd.Templates, string(data))
108+
// }
109+
// }
110+
76111
// parse the templates
77112
main := path.Base(cmd.Templates[0])
78113
templates, err := template.New(main).Funcs(functions).ParseFiles(cmd.Templates...)
@@ -88,3 +123,135 @@ func (cmd *Command) Execute(args []string) error {
88123
}
89124
return nil
90125
}
126+
127+
func read(path string) ([]byte, error) {
128+
data, err := os.ReadFile(path)
129+
if err != nil {
130+
return nil, fmt.Errorf("failed to read file: %w", err)
131+
}
132+
return data, nil
133+
}
134+
135+
// downloadHTTP downloads a file from an HTTP or HTTPS server.
136+
// The URL can be in the format:
137+
//
138+
// http://[<user>[:<password>]@]<host>[:<port>][<path>]
139+
// https://[<user>[:<password>]@]<host>[:<port>][<path>]
140+
//
141+
// where:
142+
//
143+
// - <user> is the username
144+
// - <password> is the password
145+
// - <host> is the hostname
146+
// - <port> is the port number
147+
// - <path> is the path to the file
148+
func downloadHTTP(rawURL string) ([]byte, error) {
149+
// send the GET request
150+
resp, err := http.Get(rawURL)
151+
if err != nil {
152+
return nil, fmt.Errorf("failed to make GET request: %w", err)
153+
}
154+
155+
// ensure the body is closed to prevent memory leaks
156+
defer resp.Body.Close()
157+
158+
// check for a successful status code
159+
if resp.StatusCode != http.StatusOK {
160+
return nil, fmt.Errorf("bad status: %s", resp.Status)
161+
}
162+
163+
// read the entire body into a []byte
164+
data, err := io.ReadAll(resp.Body)
165+
if err != nil {
166+
return nil, fmt.Errorf("failed to read response body: %w", err)
167+
}
168+
169+
return data, nil
170+
}
171+
172+
// downloadFTP downloads a file from an FTP or SFTP server.
173+
// The URL can be in the format:
174+
//
175+
// ftp://[<user>[:<password>]@]<host>[:<port>][<path>]
176+
// ftps://[<user>[:<password>]@]<host>[:<port>][<path>]
177+
// sftp://[<user>[:<password>]@]<host>[:<port>][<path>]
178+
//
179+
// where:
180+
//
181+
// - <user> is the username
182+
// - <password> is the password
183+
// - <host> is the hostname
184+
// - <port> is the port number
185+
// - <path> is the path to the file
186+
func downloadFTP(rawURL string) ([]byte, error) {
187+
u, err := url.Parse(rawURL)
188+
if err != nil {
189+
return nil, fmt.Errorf("invalid URL: %w", err)
190+
}
191+
192+
scheme := strings.ToLower(u.Scheme)
193+
194+
user := ""
195+
pass := ""
196+
if u.User != nil {
197+
if u.User.Username() != "" && user == "" {
198+
user = u.User.Username()
199+
}
200+
if p, ok := u.User.Password(); ok && pass == "" {
201+
pass = p
202+
}
203+
}
204+
205+
var useTLS bool
206+
switch scheme {
207+
case "ftp":
208+
useTLS = false
209+
case "ftps":
210+
useTLS = true
211+
default:
212+
return nil, fmt.Errorf("unsupported protocol for FTP download: %s", scheme)
213+
}
214+
215+
host := u.Host
216+
path := u.Path
217+
218+
if !strings.Contains(host, ":") {
219+
if useTLS {
220+
host += ":990"
221+
} else {
222+
host += ":21"
223+
}
224+
}
225+
226+
c, err := ftp.Dial(host, ftp.DialWithTimeout(10*time.Second))
227+
if err != nil {
228+
return nil, fmt.Errorf("ftp dial: %w", err)
229+
}
230+
defer c.Quit()
231+
232+
if useTLS {
233+
err = c.AuthTLS(&tls.Config{InsecureSkipVerify: true})
234+
if err != nil {
235+
return nil, fmt.Errorf("ftp auth tls: %w", err)
236+
}
237+
}
238+
239+
// Login with anonymous as default if user is empty
240+
if user == "" {
241+
user = "anonymous"
242+
pass = "anonymous"
243+
}
244+
245+
err = c.Login(user, pass)
246+
if err != nil {
247+
return nil, fmt.Errorf("ftp login: %w", err)
248+
}
249+
250+
r, err := c.Retr(path)
251+
if err != nil {
252+
return nil, fmt.Errorf("ftp retrieve: %w", err)
253+
}
254+
defer r.Close()
255+
256+
return io.ReadAll(r)
257+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/dihedron/rawdata v1.0.3
99
github.com/jedib0t/go-pretty/v6 v6.7.8
1010
github.com/jessevdk/go-flags v1.6.1
11+
github.com/jlaffaye/ftp v0.2.0
1112
github.com/joho/godotenv v1.5.1
1213
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
1314
)
@@ -18,6 +19,8 @@ require (
1819
github.com/Masterminds/semver/v3 v3.4.0 // indirect
1920
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
2021
github.com/google/uuid v1.6.0 // indirect
22+
github.com/hashicorp/errwrap v1.0.0 // indirect
23+
github.com/hashicorp/go-multierror v1.1.1 // indirect
2124
github.com/huandu/xstrings v1.5.0 // indirect
2225
github.com/mattn/go-runewidth v0.0.21 // indirect
2326
github.com/mitchellh/copystructure v1.2.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
2020
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2121
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2222
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
23+
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
24+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
25+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
26+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
2327
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
2428
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
2529
github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o=
2630
github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
2731
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
2832
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
33+
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
34+
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
2935
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
3036
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
3137
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=

0 commit comments

Comments
 (0)