M Makefile => Makefile +2 -2
@@ 1,5 1,5 @@
-# pcf
-# Copyright (C) 2019 Dakota Walsh
+# pcf - A command line sha1/(S)FTP-based pastebin client.
+# Copyright (C) 2022 Dakota Walsh
# GPL3+ See LICENSE in this repo for details.
.POSIX:
M README.md => README.md +28 -30
@@ 1,30 1,34 @@
-pcf
-====
-
-A simple command line sha1/FTP-based pastebin client. Reads file(s) from STDIN or
-filename as argument, uploads file(s) to a server, and prints the url(s) to
+# pcf
+A command line sha1/(S)FTP-based pastebin client. Reads from STDIN or a list of
+filenames as arguments, uploads files to a server, and prints the urls to
STDOUT.
-The PCFSERVER environment variable is used to declare server information. An
-example in your shellrc would be `export
-PCFSERVER='https://paste.example.com:21/incoming'`. The port and path are optional
-and will depend on how the pcf server you're using is configured.
-
-Checkout [paste.nilsu.org](https://paste.nilsu.org) for a free public pcf
-server. You can also create your own pcf server with `incron`, (anonymous) `ftpd`,
-and a script to move and rename the file to it's `SHA1.extension`. Here's [the
-script used on paste.nilsu.org](https://paste.nilsu.org/rename.py).
+Server information is configured using a simple `toml` format in
+`$XDG_CONFIG_HOME/pcf/config.toml` which is typically
+`$HOME/.config/pcf/config.toml`. An example configuration is shown below with
+the details for a free public pcf server
+[paste.nilsu.org](https://paste.nilsu.org).
-License
---------
+## Config
+```toml
+ftp_url = "ftp://paste.nilsu.org:21/incoming"
+sftp_anon_url = "sftp://paste.nilsu.org:22/incoming"
+sftp_auth_url = "sftp://paste.nilsu.org:22/var/www/html/paste"
+sftp_user = "kota"
+sftp_pass = "cowscows"
-GPL3+ see LICENSE in this repo for more details.
+# Default mode. Options are "ftp", "sftp-anon", and "sftp-auth".
+default_mode = "sftp-anon"
-Build
-------
+# Prefix for the resulting file sha1 name.
+output = "https://paste.nilsu.org/"
+```
-Build dependencies
+## License
+GPL3+ see LICENSE in this repo for more details.
+## Build
+Build dependencies:
* golang
* make
* sed
@@ 32,22 36,16 @@ Build dependencies
`make all`
-Install
---------
-
-Optionally configure `config.mk` to specify a different install location.
+## Install
+Optionally configure `config.mk` to specify a different install location.\
Defaults to `/usr/local/`
`sudo make install`
-Uninstall
-----------
-
+## Uninstall
`sudo make uninstall`
-Resources
-----------
-
+## Resources
[Send patches](https://git-send-email.io) and questions to
[~kota/pcf@lists.sr.ht](https://lists.sr.ht/~kota/pcf).
A config.go => config.go +63 -0
@@ 0,0 1,63 @@
+// pcf - A command line sha1/(S)FTP-based pastebin client.
+// Copyright (C) 2022 Dakota Walsh
+// GPL3+ See LICENSE in this repo for details.
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/BurntSushi/toml"
+)
+
+// Config a user's options for pcf.
+type Config struct {
+ // URL for anonymous FTP uploads. Anonymous FTP is an FTP server with an
+ // account (defaulting to anonymous) which has no password.
+ FtpURL string `toml:"ftp_url"`
+
+ // URL for anonymous SFTP uploads. Anonymous SFTP is an SFTP server with an
+ // account (defaulting to anonymous) which has no password.
+ SftpAnonURL string `toml:"sftp_anon_url"`
+
+ // URL for authenticated SFTP uploads.
+ SftpAuthURL string `toml:"sftp_auth_url"`
+
+ // Username for authenticated SFTP uploads.
+ SftpUser string `toml:"sftp_user"`
+
+ // Password for authenticated SFTP uploads.
+ SftpPass string `toml:"sftp_pass"`
+
+ // Options are "ftp", "sftp-anon", and "sftp-auth".
+ // When using "sftp-auth": SftpUser and SftpPass are used for
+ // authentication.
+ //
+ // If unset "ftp" will be used as the default mode.
+ DefaultMode string `toml:"default_mode"`
+
+ // Output is a prefix, typically a url, to be printed before the new
+ // filename.
+ Output string `toml:"output"`
+}
+
+// LoadConfig reads a config from a filename and returns a Config.
+func LoadConfig(filename string) (*Config, error) {
+ c := &Config{}
+ f, err := os.Open(filename)
+ if err != nil {
+ return c, fmt.Errorf("failed opening config file: %v", err)
+ }
+
+ t := toml.NewDecoder(f)
+ _, err = t.Decode(c) // TOML metadata is ignored.
+ if err != nil {
+ return c, fmt.Errorf("failed parsing config file: %v", err)
+ }
+
+ if err := f.Close(); err != nil {
+ return c, fmt.Errorf("failed closing config file: %v", err)
+ }
+
+ return c, err
+}
M config.mk => config.mk +3 -3
@@ 1,7 1,7 @@
-# pcf
-# Copyright (C) 2019 Dakota Walsh
+# pcf - A command line sha1/FTP-based pastebin client.
+# Copyright (C) 2022 Dakota Walsh
# GPL3+ See LICENSE in this repo for details.
-VERSION = 2.0.4
+VERSION = 3.0.0
# Customize below to fit your system
A config_test.go => config_test.go +57 -0
@@ 0,0 1,57 @@
+// pcf - A command line sha1/(S)FTP-based pastebin client.
+// Copyright (C) 2022 Dakota Walsh
+// GPL3+ See LICENSE in this repo for details.
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+var (
+ testConfigData = []byte(`ftp_url = "ftp://paste.nilsu.org:21/incoming"
+sftp_anon_url = "sftp://paste.nilsu.org:22/incoming"
+sftp_auth_url = "sftp://paste.nilsu.org:22/var/www/html/paste"
+sftp_user = "kota"
+sftp_pass = "cowscows"
+# Default mode. Options are "ftp" "sftp-anon" and "sftp-auth".
+default_mode = "sftp-anon"
+output = "https://paste.nilsu.org/"`)
+)
+
+func TestLoadConfig(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.toml")
+ os.WriteFile(
+ configPath,
+ testConfigData,
+ 0666,
+ )
+
+ c, err := LoadConfig(configPath)
+ if err != nil {
+ t.Fatalf("failed reading config at %s: %v\n", configPath, err)
+ }
+
+ if c.FtpURL != "ftp://paste.nilsu.org:21/incoming" {
+ t.Fatal("incorrect ftp_url from testConfigFile")
+ }
+ if c.SftpAnonURL != "sftp://paste.nilsu.org:22/incoming" {
+ t.Fatal("incorrect sftp_anon_url from testConfigFile")
+ }
+ if c.SftpAuthURL != "sftp://paste.nilsu.org:22/var/www/html/paste" {
+ t.Fatal("incorrect sftp_auth_url from testConfigFile")
+ }
+ if c.SftpUser != "kota" {
+ t.Fatal("incorrect sftp_user from testConfigFile")
+ }
+ if c.SftpPass != "cowscows" {
+ t.Fatal("incorrect sftp_pass from testConfigFile")
+ }
+ if c.DefaultMode != "sftp-anon" {
+ t.Fatal("incorrect default_mode from testConfigFile")
+ }
+ if c.Output != "https://paste.nilsu.org/" {
+ t.Fatal("incorrect output from testConfigFile")
+ }
+}
M go.mod => go.mod +8 -2
@@ 1,5 1,11 @@
module git.sr.ht/~kota/pcf
-go 1.12
+go 1.13
-require github.com/jlaffaye/ftp v0.0.0-20210307004419-5d4190119067
+require (
+ github.com/BurntSushi/toml v1.0.0
+ github.com/adrg/xdg v0.4.0
+ github.com/jlaffaye/ftp v0.0.0-20210307004419-5d4190119067
+ github.com/pkg/sftp v1.13.4
+ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b
+)
M go.sum => go.sum +21 -1
@@ 1,12 1,32 @@
+github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
+github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
+github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jlaffaye/ftp v0.0.0-20210307004419-5d4190119067 h1:P2S26PMwXl8+ZGuOG3C69LG4be5vHafUayZm9VPw3tU=
github.com/jlaffaye/ftp v0.0.0-20210307004419-5d4190119067/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
+github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
+github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
+golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
M main.go => main.go +118 -105
@@ 1,143 1,156 @@
-// pcf - A powerful paste.cf command line client
-// Copyright (C) 2019 Dakota Walsh
+// pcf - A command line sha1/(S)FTP-based pastebin client.
+// Copyright (C) 2022 Dakota Walsh
// GPL3+ See LICENSE in this repo for details.
package main
import (
"bytes"
"crypto/sha1"
+ "flag"
"fmt"
"io"
- "net/url"
+ "math/rand"
"os"
- "path"
"path/filepath"
- "time"
+ "strings"
- "github.com/jlaffaye/ftp"
+ "github.com/adrg/xdg"
)
func main() {
- addr := os.Getenv("PCFSERVER")
- if addr == "" {
- fmt.Println("pcf: you must set the PCFSERVER environment variable!")
+ // Find and read config.
+ configPath, err := xdg.ConfigFile("pcf/config.toml")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed reading config directory: %v\n", err)
os.Exit(1)
}
- // parse the ftpURL
- ftpURL, err := url.Parse(addr)
+ config, err := LoadConfig(configPath)
if err != nil {
- fmt.Fprintf(os.Stderr, "pcf: url configuration wrong: %v\n", err)
+ fmt.Fprintf(os.Stderr, "failed loading config file: %v\n", err)
+ os.Exit(1)
}
- args := os.Args[1:]
- if len(args) == 0 {
- // use stdin data
- inBytes, err := ReadAll(os.Stdin)
+ // Parse flags and override config.DefaultMode if needed.
+ ftpFlag := flag.Bool("f", false, "use anonymous FTP")
+ sftpFlag := flag.Bool("s", false, "use anonymous SFTP")
+ authFlag := flag.Bool("a", false, "use authenticated SFTP")
+
+ flag.Parse()
+
+ mode := config.DefaultMode
+ switch {
+ case *authFlag:
+ mode = "sftp-auth"
+ case *sftpFlag:
+ mode = "sftp-anon"
+ case *ftpFlag:
+ mode = "ftp"
+ }
+
+ // Select the correct upload URL.
+ var url string
+ switch mode {
+ case "sftp-auth":
+ url = config.SftpAuthURL
+ case "sftp-anon":
+ url = config.SftpAnonURL
+ default:
+ url = config.FtpURL
+ }
+
+ // Create requests and upload each file.
+ filePaths := flag.Args()
+ UploadFiles(
+ mode,
+ url,
+ config.SftpUser,
+ config.SftpPass,
+ config.Output,
+ filePaths,
+ )
+}
+
+// UploadFiles uploads a list of files by path sequentially in the order given.
+// After each successful upload, the resulting uploaded URL is printed to
+// STDOUT. Any upload errors will cause an error to be printed to Stderr and
+// exit with an error code.
+func UploadFiles(mode, url, user, pass, output string, paths []string) {
+ for _, path := range paths {
+ f, err := os.Open(path)
if err != nil {
- fmt.Fprintf(os.Stderr, "pcf: failed reading stdin: %v\n", err)
+ fmt.Fprintf(os.Stderr, "failed opening %s: %v\n", path, err)
os.Exit(1)
}
- // create reader (that supports seek) from stdinBytes
- in := bytes.NewReader(inBytes)
- connection := login(ftpURL)
- store(ftpURL, in, connection, "file")
- exit(connection)
- // calculate the hash (setting the reader to byte 0 first)
- if _, err := in.Seek(0, 0); err != nil {
- fmt.Fprintf(os.Stderr, "pcf: failed to read: %v\n", err)
+
+ var buf bytes.Buffer
+ tee := io.TeeReader(f, &buf)
+
+ name := filepath.Base(path)
+ if name == "" || name == "." {
+ name = RandString(5)
+ }
+
+ req, err := NewRequest(mode, url, user, pass, name, tee)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed creating upload request for %s: %v\n", path, err)
os.Exit(1)
}
- h := hash(in)
- webURL := *ftpURL
- webURL.Host = ftpURL.Hostname()
- webURL.Path = h
- fmt.Println(&webURL)
- } else {
- // loop through and use all arguments
- connection := login(ftpURL)
- for _, arg := range args {
- file, err := os.Open(arg)
- if err != nil {
- fmt.Fprintf(os.Stderr, "pcf: open: %v\n", err)
- continue
- }
- defer file.Close()
-
- // store the file
- store(ftpURL, file, connection, filepath.Base(arg))
-
- // calculate the hash (setting the reader to byte 0 first)
- if _, err := file.Seek(0, 0); err != nil {
- fmt.Fprintf(os.Stderr, "pcf: failed to read: %v\n", err)
- os.Exit(1)
- }
- h := hash(file)
-
- // print the URL
- webURL := *ftpURL
- webURL.Host = ftpURL.Hostname()
- webURL.Path = h + filepath.Ext(arg)
- fmt.Println(&webURL)
+
+ err = req.Upload()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed while uploading %s: %v\n", path, err)
+ os.Exit(1)
}
- exit(connection)
- }
-}
-// create the ftp connection
-func login(u *url.URL) *ftp.ServerConn {
- c, err := ftp.Dial(u.Host, ftp.DialWithTimeout(10*time.Second))
- if err != nil {
- fmt.Fprintf(os.Stderr, "pcf: %v\n", err)
- }
- err = c.Login("anonymous", "anonymous")
- if err != nil {
- fmt.Fprintf(os.Stderr, "pcf: login: %v\n", err)
- }
- return c
-}
+ if err := f.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "failed closing %s: %v\n", path, err)
+ os.Exit(1)
+ }
-// store the file in the connection
-func store(u *url.URL, r io.Reader, c *ftp.ServerConn, n string) {
- err := c.Stor(path.Join(u.Path, n), r)
- if err != nil {
- fmt.Fprintf(os.Stderr, "pcf: put: %v\n", err)
+ // Success! Print the output URL.
+ fmt.Println(HashName(&buf, path, output))
}
}
-// calculate and print the hash
-func hash(f io.Reader) string {
- h := sha1.New()
- if _, err := io.Copy(h, f); err != nil {
- fmt.Fprintf(os.Stderr, "pcf: failed to hash: %v\n", err)
+// RandString returns a random latin string of a variable length.
+func RandString(n int) string {
+ const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
- return fmt.Sprintf("%x", h.Sum(nil))
+ return string(b)
}
-// close the ftp connection
-func exit(c *ftp.ServerConn) {
- err := c.Quit()
- if err != nil {
- fmt.Fprintf(os.Stderr, "pcf: quit: %v\n", err)
+// HashName combines a prefix, SHA1 hash from an io.Reader, and the file
+// extension from a path.
+//
+// The result is typically a URL indicating where the uploaded file can be
+// located.
+func HashName(f io.Reader, path string, prefix string) string {
+ var b strings.Builder
+ b.WriteString(prefix)
+
+ hash := sha1.New()
+ if _, err := io.Copy(hash, f); err != nil {
+ fmt.Fprintf(
+ os.Stderr,
+ "failed calculating hash for %s: %v\n",
+ path,
+ err,
+ )
}
+ b.WriteString(fmt.Sprintf("%x", hash.Sum(nil)))
+
+ b.WriteString(Ext(path))
+ return b.String()
}
-// ReadAll is from go 1.16
-// Implementing it ourself allows building in older go versions.
-func ReadAll(r io.Reader) ([]byte, error) {
- b := make([]byte, 0, 512)
- for {
- if len(b) == cap(b) {
- // Add more capacity (let append pick how much).
- b = append(b, 0)[:len(b)]
- }
- n, err := r.Read(b[len(b):cap(b)])
- b = b[:len(b)+n]
- if err != nil {
- if err == io.EOF {
- err = nil
- }
- return b, err
- }
- }
+// Ext returns the file name extension used by path.
+// Unlike filepath.Ext we return a blank string for hidden files without an
+// extension (.gitignore for example is considered to not have an extension).
+func Ext(path string) string {
+ path = strings.TrimPrefix(path, ".")
+ return filepath.Ext(path)
}
A request.go => request.go +176 -0
@@ 0,0 1,176 @@
+// pcf - A command line sha1/(S)FTP-based pastebin client.
+// Copyright (C) 2022 Dakota Walsh
+// GPL3+ See LICENSE in this repo for details.
+package main
+
+import (
+ "fmt"
+ "io"
+ neturl "net/url"
+ "path/filepath"
+ "time"
+
+ "github.com/jlaffaye/ftp"
+ "github.com/pkg/sftp"
+ "golang.org/x/crypto/ssh"
+)
+
+// A Request to upload a file.
+type Request struct {
+ // Mode describes the type of request to be made.
+ Mode RequestMode
+
+ // URL encodes the upload server, path, username, and password. The path is
+ // used to change the directory where the file will be written on the
+ // server.
+ URL *neturl.URL
+
+ // Name to store the file as on the remote server. This name is overwritten
+ // in all modes except RequestModeSFTPAuth. For these other modes you should
+ // use a random string in order to not have your file overwritten if another
+ // user is simultaneously uploading.
+ Name string
+
+ // Content is the data to be uploaded.
+ Content io.Reader
+}
+
+// RequestMode describes the type of request.
+type RequestMode uint8
+
+const (
+ // RequestModeFTP is a RequestMode that uploads to an FTP server with an
+ // username (defaulting to anonymous) and no password.
+ RequestModeFTP RequestMode = iota
+
+ // RequestModeSFTPAnon is a RequestMode that uploads to an SFTP server with
+ // an username (defaulting to anonymous) and no password.
+ RequestModeSFTPAnon
+
+ // RequestModeSFTPAuth is a RequestMode that uploads to an SFTP server with
+ // a username and password.
+ RequestModeSFTPAuth
+)
+
+// NewRequest returns a new Request given a mode, url, file name, an optional
+// username and password and an io.Reader.
+//
+// Mode is a string representation of a request mode:
+// "ftp" = RequestModeFTP
+// "sftp-anon" = RequestModeSFTPAnon
+// "sftp-auth" = RequestModeSFTPAuth
+// If an invalid mode is provided "ftp" is used.
+//
+// Usernames and passwords encoded within the URL will be used for any of the
+// three modes. However, if user or pass strings are given and the mode is
+// "sftp-auth" they will override the encoded credentials. For all other modes
+// given credentials are ignored.
+func NewRequest(mode string, url string, user string, pass string, name string, content io.Reader) (*Request, error) {
+ r := &Request{}
+ var err error
+
+ // Set mode and parse url.
+ switch mode {
+ case "sftp-auth":
+ r.Mode = RequestModeSFTPAuth
+ case "sftp-anon":
+ r.Mode = RequestModeSFTPAnon
+ default:
+ r.Mode = RequestModeFTP
+ }
+ r.URL, err = neturl.Parse(url)
+
+ // Replace URL encoded username and password if provided.
+ if r.Mode == RequestModeSFTPAuth && user != "" {
+ r.URL.User = neturl.UserPassword(user, pass)
+ }
+
+ r.Name = name
+ r.Content = content
+ return r, err
+}
+
+// Upload processes a request by uploading it to the FTP or SFTP server and then
+// returns nil for a successful upload; or an error.
+func (req *Request) Upload() error {
+ if req.Mode == RequestModeFTP {
+ return req.uploadFTP()
+ }
+ return req.uploadSFTP()
+}
+
+// uploadFTP processes a request using FTP to upload the file.
+func (req *Request) uploadFTP() error {
+ conn, err := ftp.Dial(req.URL.Host, ftp.DialWithTimeout(10*time.Second))
+ if err != nil {
+ return fmt.Errorf("connecting to %v: %w", req.URL.Host, err)
+ }
+
+ // Check if a password was provided in the URL. If so, use it instead of the
+ // default username and password.
+ var pass, user string
+ if urlPass, ok := req.URL.User.Password(); ok {
+ pass = urlPass
+ user = req.URL.User.Username()
+ } else {
+ pass = "anonymous"
+ user = "anonymous"
+ }
+
+ if err := conn.Login(user, pass); err != nil {
+ return fmt.Errorf("logging into anonymous ftp with %s %s: %v", user, pass, err)
+ }
+
+ // Store the content, in the path provided in the URL.
+ return conn.Stor(filepath.Join(req.URL.Path, req.Name), req.Content)
+}
+
+func (req *Request) uploadSFTP() error {
+ // Default anonymous username and blank password.
+ user := "anonymous"
+ pass := ""
+
+ // Check if a password was provided in the URL.
+ urlPass, ok := req.URL.User.Password()
+ if !ok {
+ if req.Mode == RequestModeSFTPAuth {
+ return fmt.Errorf("missing sftp password for authenticated sftp mode")
+ }
+ } else {
+ user = req.URL.User.Username()
+ pass = urlPass
+ }
+
+ config := &ssh.ClientConfig{
+ User: user,
+ Auth: []ssh.AuthMethod{
+ ssh.Password(pass),
+ },
+ HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: store hostkeys
+ }
+
+ // An ssh connection is needed to start sftp.
+ conn, err := ssh.Dial("tcp", req.URL.Host, config)
+ if err != nil {
+ return fmt.Errorf("connecting to %v: %w", req.URL.Host, err)
+ }
+ defer conn.Close()
+
+ // Start sftp and then upload the file to the given path and name.
+ client, err := sftp.NewClient(conn)
+ if err != nil {
+ return fmt.Errorf("failed initializing sftp in ssh session: %v", err)
+ }
+
+ path := sftp.Join(req.URL.Path, req.Name)
+ f, err := client.Create(path)
+ if err != nil {
+ return fmt.Errorf("failed creating file at path %s: %v", path, err)
+ }
+
+ _, err = f.ReadFrom(req.Content)
+ if err != nil {
+ return fmt.Errorf("failed writing content to file in sftp connection: %v", err)
+ }
+ return client.Close()
+}