From 9bd7b30222a173dc5169c8734a0641f093bfd5c5 Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Wed, 30 Dec 2020 23:54:18 -0800 Subject: [PATCH] Implement config files --- config.go | 282 +++++--------------------------------- extra.go | 24 ++++ files.go | 2 +- go.mod | 1 + go.sum | 4 + logging.go | 4 +- main.go | 83 ++++++----- parameterSerialization.go | 278 +++++++++++++++++++++++++++++++++++++ 8 files changed, 390 insertions(+), 288 deletions(-) create mode 100644 extra.go create mode 100644 parameterSerialization.go diff --git a/config.go b/config.go index 8b86627..b84a476 100644 --- a/config.go +++ b/config.go @@ -1,278 +1,66 @@ package main import ( - "archive/tar" - "encoding/json" - "github.com/pkg/browser" + "errors" + "github.com/pelletier/go-toml" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "io" "io/ioutil" - "net/url" "os" - "path/filepath" - "strconv" - "strings" ) -// Create config type to store action type and data type Config struct { - ActionType string - ActionData string + Receiver ReceiverConfig + Sender SenderConfig + Targets map[string]map[string]string } -// Instantiate and return a new Config struct -func NewConfig(actionType string, actionData string) *Config { - return &Config{ActionType: actionType, ActionData: actionData} +type ReceiverConfig struct { + DestDir string `toml:"destinationDirectory"` + SkipZeroconf bool + WorkDir string `toml:"workingDirectory"` } -func (config *Config) Validate() { - if config.ActionType == "url" { - // Parse URL in config - urlParser, err := url.Parse(config.ActionData) - // If there was an error parsing - if err != nil { - // Alert user of invalid url - log.Fatal().Err(err).Msg("Invalid URL") - // If scheme is not detected - } else if urlParser.Scheme == "" { - // Alert user of invalid scheme - log.Fatal().Msg("Invalid URL scheme") - // If host is not detected - } else if urlParser.Host == "" { - // Alert user of invalid host - log.Fatal().Msg("Invalid URL host") - } - } +type SenderConfig struct { + WorkDir string `toml:"workingDirectory"` } -// Create config file -func (config *Config) CreateFile(dir string) { +func GetConfigPath() string { // Use ConsoleWriter logger log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) - // Create config file at given directory - configFile, err := os.Create(dir + "/config.json") - if err != nil { - log.Fatal().Err(err).Msg("Error creating config file") + configLocations := []string{"~/.config/opensend.toml", "/etc/opensend.toml"} + for _, configLocation := range configLocations { + expandedPath := ExpandPath(configLocation) + if _, err := os.Stat(expandedPath); errors.Is(err, os.ErrNotExist) { + continue + } + return expandedPath } - // Close config file at the end of this function - defer configFile.Close() - // Marshal given Config struct into a []byte - jsonData, err := json.Marshal(config) - if err != nil { - log.Fatal().Err(err).Msg("Error encoding JSON") - } - // Write []byte to previously created config file - bytesWritten, err := configFile.Write(jsonData) - if err != nil { - log.Fatal().Err(err).Msg("Error writing JSON to file") - } - // Log bytes written - log.Info().Str("file", "config.json").Msg("Wrote " + strconv.Itoa(bytesWritten) + " bytes") + return "" } -// Collect all required files into given directory -func (config *Config) CollectFiles(dir string) { +func NewConfig(path string) *Config { // Use ConsoleWriter logger log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) - // If action type is file - if config.ActionType == "file" { - // Open file path in config.ActionData - src, err := os.Open(config.ActionData) + newConfig := &Config{} + newConfig.SetDefaults() + if path != "" { + confData, err := ioutil.ReadFile(path) if err != nil { - log.Fatal().Err(err).Msg("Error opening file from config") + log.Fatal().Err(err).Msg("Error reading config") } - // Close source file at the end of this function - defer src.Close() - // Create new file with the same name at given directory - dst, err := os.Create(dir + "/" + filepath.Base(config.ActionData)) + err = toml.Unmarshal(confData, newConfig) if err != nil { - log.Fatal().Err(err).Msg("Error creating file") + log.Fatal().Err(err).Msg("Error unmarshalling toml") } - // Close new file at the end of this function - defer dst.Close() - // Copy data from source file to destination file - _, err = io.Copy(dst, src) - if err != nil { - log.Fatal().Err(err).Msg("Error copying data to file") - } - // Replace file path in config.ActionData with file name - config.ActionData = filepath.Base(config.ActionData) - } else if config.ActionType == "dir" { - // Create tar archive - tarFile, err := os.Create(dir + "/" + filepath.Base(config.ActionData) + ".tar") - if err != nil { - log.Fatal().Err(err).Msg("Error creating file") - } - // Close tar file at the end of this function - defer tarFile.Close() - // Create writer for tar archive - tarArchiver := tar.NewWriter(tarFile) - // Close archiver at the end of this function - defer tarArchiver.Close() - // Walk given directory - err = filepath.Walk(config.ActionData, func(path string, info os.FileInfo, err error) error { - // Return if error walking - if err != nil { - return err - } - // Skip if file is not normal mode - if !info.Mode().IsRegular() { - return nil - } - // Create tar header for file - header, err := tar.FileInfoHeader(info, info.Name()) - if err != nil { - return err - } - // Change header name to reflect decompressed filepath - header.Name = strings.TrimPrefix(strings.ReplaceAll(path, config.ActionData, ""), string(filepath.Separator)) - // Write header to archive - if err := tarArchiver.WriteHeader(header); err != nil { - return err - } - // Open source file - src, err := os.Open(path) - if err != nil { - return err - } - // Close source file at the end of this function - defer src.Close() - // Copy source bytes to tar archive - if _, err := io.Copy(tarArchiver, src); err != nil { - return err - } - // Return at the end of the function - return nil - }) - if err != nil { - log.Fatal().Err(err).Msg("Error creating tar archive") - } - // Set config data to base path for receiver - config.ActionData = filepath.Base(config.ActionData) } + return newConfig } -// Read config file at given file path -func (config *Config) ReadFile(filePath string) { - // Use ConsoleWriter logger - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) - // Read file at filePath - fileData, err := ioutil.ReadFile(filePath) - if err != nil { - log.Fatal().Err(err).Msg("Error reading config file") - } - // Unmarshal data from JSON into config struct - err = json.Unmarshal(fileData, config) - if err != nil { - log.Fatal().Err(err).Msg("Error decoding JSON") - } -} - -// Execute action specified in config -func (config *Config) ExecuteAction(srcDir string, destDir string) { - // Use ConsoleWriter logger - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) - // If action is file - if config.ActionType == "file" { - // Open file from config at given directory - src, err := os.Open(srcDir + "/" + config.ActionData) - if err != nil { - log.Fatal().Err(err).Msg("Error reading file from config") - } - // Close source file at the end of this function - defer src.Close() - // Create file in user's Downloads directory - dst, err := os.Create(filepath.Clean(destDir) + "/" + config.ActionData) - if err != nil { - log.Fatal().Err(err).Msg("Error creating file") - } - // Close destination file at the end of this function - defer dst.Close() - // Copy data from source file to destination file - _, err = io.Copy(dst, src) - if err != nil { - log.Fatal().Err(err).Msg("Error copying data to file") - } - // If action is url - } else if config.ActionType == "url" { - // Parse received URL - urlParser, err := url.Parse(config.ActionData) - // If there was an error parsing - if err != nil { - // Alert user of invalid url - log.Fatal().Err(err).Msg("Invalid URL") - // If scheme is not detected - } else if urlParser.Scheme == "" { - // Alert user of invalid scheme - log.Fatal().Msg("Invalid URL scheme") - // If host is not detected - } else if urlParser.Host == "" { - // Alert user of invalid host - log.Fatal().Msg("Invalid URL host") - } - // Attempt to open URL in browser - err = browser.OpenURL(config.ActionData) - if err != nil { - log.Fatal().Err(err).Msg("Error opening browser") - } - // If action is dir - } else if config.ActionType == "dir" { - // Set destination directory to ~/Downloads/{dir name} - dstDir := filepath.Clean(destDir) + "/" + config.ActionData - // Try to create destination directory - err := os.MkdirAll(dstDir, 0755) - if err != nil { - log.Fatal().Err(err).Msg("Error creating directory") - } - // Try to open tar archive file - tarFile, err := os.Open(srcDir + "/" + config.ActionData + ".tar") - if err != nil { - log.Fatal().Err(err).Msg("Error opening tar archive") - } - // Close tar archive file at the end of this function - defer tarFile.Close() - // Create tar reader to unarchive tar archive - tarUnarchiver := tar.NewReader(tarFile) - // Loop to recursively unarchive tar file - unarchiveLoop: - for { - // Jump to next header in tar archive - header, err := tarUnarchiver.Next() - switch { - // If EOF - case err == io.EOF: - // break loop - break unarchiveLoop - case err != nil: - log.Fatal().Err(err).Msg("Error unarchiving tar archive") - // If nil header - case header == nil: - // Skip - continue - } - // Set target path to header name in destination dir - targetPath := filepath.Join(dstDir, header.Name) - switch header.Typeflag { - // If regular file - case tar.TypeReg: - // Try to create containing folder ignoring errors - _ = os.MkdirAll(filepath.Dir(targetPath), 0755) - // Create file with mode contained in header at target path - dstFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) - if err != nil { - log.Fatal().Err(err).Msg("Error creating file during unarchiving") - } - // Copy data from tar archive into file - _, err = io.Copy(dstFile, tarUnarchiver) - if err != nil { - log.Fatal().Err(err).Msg("Error copying data to file") - } - } - } - // Catchall - } else { - // Log unknown action type - log.Fatal().Msg("Unknown action type " + config.ActionType) - } +func (config *Config) SetDefaults() { + config.Receiver.DestDir = ExpandPath("~/Downloads") + config.Receiver.WorkDir = ExpandPath("~/.opensend") + config.Receiver.SkipZeroconf = false + config.Sender.WorkDir = ExpandPath("~/.opensend") + config.Targets = map[string]map[string]string{} } diff --git a/extra.go b/extra.go new file mode 100644 index 0000000..98d1f79 --- /dev/null +++ b/extra.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "os" + "path/filepath" + "strings" +) + +func ExpandPath(s string) string { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) + homeDir, err := os.UserHomeDir() + if err != nil { + log.Fatal().Err(err).Msg("Error getting home directory") + } + expandedString := os.ExpandEnv(s) + if strings.HasPrefix(expandedString, "~") { + expandedString = strings.Replace(expandedString, "~", homeDir, 1) + } + expandedString = filepath.Clean(expandedString) + return expandedString +} diff --git a/files.go b/files.go index ab04091..6546d80 100644 --- a/files.go +++ b/files.go @@ -223,7 +223,7 @@ func RecvFiles(connection net.Conn) { // Otherwise } else { // Create new file at index filepath - newFile, err := os.Create(opensendDir + "/" + file) + newFile, err := os.Create(*workDir + "/" + file) if err != nil { log.Fatal().Err(err).Msg("Error creating file") } diff --git a/go.mod b/go.mod index 988eab4..dd34f4b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.15 require ( github.com/grandcat/zeroconf v1.0.0 github.com/klauspost/compress v1.11.3 + github.com/pelletier/go-toml v1.8.1 github.com/pkg/browser v0.0.0-20201112035734-206646e67786 github.com/rs/zerolog v1.20.0 ) diff --git a/go.sum b/go.sum index cf6f34e..ff12b29 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,16 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pkg/browser v0.0.0-20201112035734-206646e67786 h1:4Gk0Dsp90g2YwfsxDOjvkEIgKGh+2R9FlvormRycveA= github.com/pkg/browser v0.0.0-20201112035734-206646e67786/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/logging.go b/logging.go index b564bd1..a1030b0 100644 --- a/logging.go +++ b/logging.go @@ -15,7 +15,7 @@ func (hook FatalHook) Run(_ *zerolog.Event, level zerolog.Level, _ string) { // If log event is fatal if level == zerolog.FatalLevel { // Attempt removal of opensend directory - _ = os.RemoveAll(opensendDir) + _ = os.RemoveAll(*workDir) } } @@ -33,7 +33,7 @@ func (hook TCPFatalHook) Run(_ *zerolog.Event, level zerolog.Level, _ string) { // Close connection _ = hook.conn.Close() // Attempt removal of opensend directory - _ = os.RemoveAll(opensendDir) + _ = os.RemoveAll(*workDir) } } diff --git a/main.go b/main.go index e4e5ce7..1141c15 100644 --- a/main.go +++ b/main.go @@ -17,19 +17,43 @@ import ( "time" ) -var opensendDir string +var workDir *string func main() { // Use ConsoleWriter logger log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) - // Get user's home directory - homeDir, err := os.UserHomeDir() - if err != nil { - log.Fatal().Err(err).Msg("Error getting home directory") + confPath := GetConfigPath() + config := NewConfig(confPath) + + // Create --send-to flag to send to a specific IP + sendTo := flag.String("send-to", "", "Use IP address of receiver instead of mDNS") + // Create --dest-dir flag to save to a specified folder + destDir := flag.String("dest-dir", "", "Destination directory for files or dirs sent over opensend") + workDir = flag.String("work-dir", "", "Working directory for opensend") + givenCfgPath := flag.String("config", "", "Opensend config to use") + // Create --skip-mdns to skip service registration + skipMdns := flag.Bool("skip-mdns", config.Receiver.SkipZeroconf, "Skip zeroconf service registration (use if mdns fails)") + // Create -t flag for type + actionType := flag.String("t", "", "Type of data being sent") + // Create -d flag for data + actionData := flag.String("d", "", "Data to send") + // Create -s flag for sending + sendFlag := flag.Bool("s", false, "Send data") + // Create -r flag for receiving + recvFlag := flag.Bool("r", false, "Receive data") + // Parse flags + flag.Parse() + if *givenCfgPath != "" { + config = NewConfig(*givenCfgPath) + } + if *workDir == "" { + if *sendFlag { + *workDir = ExpandPath(config.Sender.WorkDir) + } else { + *workDir = ExpandPath(config.Receiver.WorkDir) + } } - // Define opensend directory as ~/.opensend - opensendDir = homeDir + "/.opensend" // Create channel for signals sig := make(chan os.Signal, 1) @@ -43,31 +67,14 @@ func main() { // Warn user that a signal has been received and that opensend is shutting down log.Warn().Msg("Signal received. Shutting down.") // Remove opensend directory to avoid future conflicts - _ = os.RemoveAll(opensendDir) + _ = os.RemoveAll(*workDir) // Exit with code 0 os.Exit(0) } }() - // Create --send-to flag to send to a specific IP - sendTo := flag.String("send-to", "", "Use IP address of receiver instead of mDNS") - // Create --dest-dir flag to save to a specified folder - destDir := flag.String("dest-dir", homeDir+"/Downloads", "Destination directory for files or dirs sent over opensend") - // Create --skip-mdns to skip service registration - skipMdns := flag.Bool("skip-mdns", false, "Skip zeroconf service registration (use if mdns fails)") - // Create -t flag for type - actionType := flag.String("t", "", "Type of data being sent") - // Create -d flag for data - actionData := flag.String("d", "", "Data to send") - // Create -s flag for sending - sendFlag := flag.Bool("s", false, "Send data") - // Create -r flag for receiving - recvFlag := flag.Bool("r", false, "Receive data") - // Parse flags - flag.Parse() - // Create opensend dir ignoring errors - _ = os.Mkdir(opensendDir, 0755) + _ = os.Mkdir(*workDir, 0755) // If -s given if *sendFlag { if *actionType == "" || *actionData == "" { @@ -119,13 +126,13 @@ func main() { choiceIP = discoveredIPs[choiceIndex] } // Instantiate Config object - config := NewConfig(*actionType, *actionData) + parameters := NewParameters(*actionType, *actionData) // Validate data in config struct - config.Validate() + parameters.Validate() // Collect any files that may be required for transaction into opensend directory - config.CollectFiles(opensendDir) + parameters.CollectFiles(*workDir) // Create config file in opensend directory - config.CreateFile(opensendDir) + parameters.CreateFile(*workDir) // Notify user of key exchange log.Info().Msg("Performing key exchange") // Exchange RSA keys with receiver @@ -135,15 +142,15 @@ func main() { // Encrypt shared key using RSA public key key := EncryptKey(sharedKey, rawKey) // Save encrypted key in opensend directory as key.aes - SaveEncryptedKey(key, opensendDir+"/key.aes") + SaveEncryptedKey(key, *workDir+"/key.aes") // Notify user file encryption is beginning log.Info().Msg("Encrypting files") // Encrypt all files in opensend directory using shared key - EncryptFiles(opensendDir, sharedKey) + EncryptFiles(*workDir, sharedKey) // Notify user server has started log.Info().Msg("Server started on port 9898") // Send all files in opensend directory using an HTTP server on port 9898 - SendFiles(opensendDir) + SendFiles(*workDir) // If -r given } else if *recvFlag { // If --skip-mdns is not given @@ -178,21 +185,21 @@ func main() { // Notify user file decryption is beginning log.Info().Msg("Decrypting files") // Decrypt all files in opensend directory using shared key - DecryptFiles(opensendDir, sharedKey) + DecryptFiles(*workDir, sharedKey) // Instantiate Config - config := &Config{} + parameters := &Parameters{} // Read config file in opensend directory - config.ReadFile(opensendDir + "/config.json") + parameters.ReadFile(*workDir + "/parameters.json") // Notify user that action is being executed log.Info().Msg("Executing JSON action") // Execute JSON action using files within opensend directory - config.ExecuteAction(opensendDir, *destDir) + parameters.ExecuteAction(*workDir, *destDir) } else { flag.Usage() log.Fatal().Msg("You must choose sender or receiver mode using -s or -r") } // Remove opensend directory - err = os.RemoveAll(opensendDir) + err := os.RemoveAll(*workDir) if err != nil { log.Fatal().Err(err).Msg("Error removing opensend directory") } diff --git a/parameterSerialization.go b/parameterSerialization.go new file mode 100644 index 0000000..9f9240e --- /dev/null +++ b/parameterSerialization.go @@ -0,0 +1,278 @@ +package main + +import ( + "archive/tar" + "encoding/json" + "github.com/pkg/browser" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "io" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" +) + +// Create config type to store action type and data +type Parameters struct { + ActionType string + ActionData string +} + +// Instantiate and return a new Config struct +func NewParameters(actionType string, actionData string) *Parameters { + return &Parameters{ActionType: actionType, ActionData: actionData} +} + +func (parameters *Parameters) Validate() { + if parameters.ActionType == "url" { + // Parse URL in parameters + urlParser, err := url.Parse(parameters.ActionData) + // If there was an error parsing + if err != nil { + // Alert user of invalid url + log.Fatal().Err(err).Msg("Invalid URL") + // If scheme is not detected + } else if urlParser.Scheme == "" { + // Alert user of invalid scheme + log.Fatal().Msg("Invalid URL scheme") + // If host is not detected + } else if urlParser.Host == "" { + // Alert user of invalid host + log.Fatal().Msg("Invalid URL host") + } + } +} + +// Create config file +func (parameters *Parameters) CreateFile(dir string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) + // Create parameters file at given directory + configFile, err := os.Create(dir + "/parameters.json") + if err != nil { + log.Fatal().Err(err).Msg("Error creating parameters file") + } + // Close parameters file at the end of this function + defer configFile.Close() + // Marshal given Parameters struct into a []byte + jsonData, err := json.Marshal(parameters) + if err != nil { + log.Fatal().Err(err).Msg("Error encoding JSON") + } + // Write []byte to previously created parameters file + bytesWritten, err := configFile.Write(jsonData) + if err != nil { + log.Fatal().Err(err).Msg("Error writing JSON to file") + } + // Log bytes written + log.Info().Str("file", "parameters.json").Msg("Wrote " + strconv.Itoa(bytesWritten) + " bytes") +} + +// Collect all required files into given directory +func (parameters *Parameters) CollectFiles(dir string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) + // If action type is file + if parameters.ActionType == "file" { + // Open file path in parameters.ActionData + src, err := os.Open(parameters.ActionData) + if err != nil { + log.Fatal().Err(err).Msg("Error opening file from parameters") + } + // Close source file at the end of this function + defer src.Close() + // Create new file with the same name at given directory + dst, err := os.Create(dir + "/" + filepath.Base(parameters.ActionData)) + if err != nil { + log.Fatal().Err(err).Msg("Error creating file") + } + // Close new file at the end of this function + defer dst.Close() + // Copy data from source file to destination file + _, err = io.Copy(dst, src) + if err != nil { + log.Fatal().Err(err).Msg("Error copying data to file") + } + // Replace file path in parameters.ActionData with file name + parameters.ActionData = filepath.Base(parameters.ActionData) + } else if parameters.ActionType == "dir" { + // Create tar archive + tarFile, err := os.Create(dir + "/" + filepath.Base(parameters.ActionData) + ".tar") + if err != nil { + log.Fatal().Err(err).Msg("Error creating file") + } + // Close tar file at the end of this function + defer tarFile.Close() + // Create writer for tar archive + tarArchiver := tar.NewWriter(tarFile) + // Close archiver at the end of this function + defer tarArchiver.Close() + // Walk given directory + err = filepath.Walk(parameters.ActionData, func(path string, info os.FileInfo, err error) error { + // Return if error walking + if err != nil { + return err + } + // Skip if file is not normal mode + if !info.Mode().IsRegular() { + return nil + } + // Create tar header for file + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return err + } + // Change header name to reflect decompressed filepath + header.Name = strings.TrimPrefix(strings.ReplaceAll(path, parameters.ActionData, ""), string(filepath.Separator)) + // Write header to archive + if err := tarArchiver.WriteHeader(header); err != nil { + return err + } + // Open source file + src, err := os.Open(path) + if err != nil { + return err + } + // Close source file at the end of this function + defer src.Close() + // Copy source bytes to tar archive + if _, err := io.Copy(tarArchiver, src); err != nil { + return err + } + // Return at the end of the function + return nil + }) + if err != nil { + log.Fatal().Err(err).Msg("Error creating tar archive") + } + // Set parameters data to base path for receiver + parameters.ActionData = filepath.Base(parameters.ActionData) + } +} + +// Read config file at given file path +func (parameters *Parameters) ReadFile(filePath string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) + // Read file at filePath + fileData, err := ioutil.ReadFile(filePath) + if err != nil { + log.Fatal().Err(err).Msg("Error reading parameters file") + } + // Unmarshal data from JSON into parameters struct + err = json.Unmarshal(fileData, parameters) + if err != nil { + log.Fatal().Err(err).Msg("Error decoding JSON") + } +} + +// Execute action specified in config +func (parameters *Parameters) ExecuteAction(srcDir string, destDir string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{}) + // If action is file + if parameters.ActionType == "file" { + // Open file from parameters at given directory + src, err := os.Open(srcDir + "/" + parameters.ActionData) + if err != nil { + log.Fatal().Err(err).Msg("Error reading file from parameters") + } + // Close source file at the end of this function + defer src.Close() + // Create file in user's Downloads directory + dst, err := os.Create(filepath.Clean(destDir) + "/" + parameters.ActionData) + if err != nil { + log.Fatal().Err(err).Msg("Error creating file") + } + // Close destination file at the end of this function + defer dst.Close() + // Copy data from source file to destination file + _, err = io.Copy(dst, src) + if err != nil { + log.Fatal().Err(err).Msg("Error copying data to file") + } + // If action is url + } else if parameters.ActionType == "url" { + // Parse received URL + urlParser, err := url.Parse(parameters.ActionData) + // If there was an error parsing + if err != nil { + // Alert user of invalid url + log.Fatal().Err(err).Msg("Invalid URL") + // If scheme is not detected + } else if urlParser.Scheme == "" { + // Alert user of invalid scheme + log.Fatal().Msg("Invalid URL scheme") + // If host is not detected + } else if urlParser.Host == "" { + // Alert user of invalid host + log.Fatal().Msg("Invalid URL host") + } + // Attempt to open URL in browser + err = browser.OpenURL(parameters.ActionData) + if err != nil { + log.Fatal().Err(err).Msg("Error opening browser") + } + // If action is dir + } else if parameters.ActionType == "dir" { + // Set destination directory to ~/Downloads/{dir name} + dstDir := filepath.Clean(destDir) + "/" + parameters.ActionData + // Try to create destination directory + err := os.MkdirAll(dstDir, 0755) + if err != nil { + log.Fatal().Err(err).Msg("Error creating directory") + } + // Try to open tar archive file + tarFile, err := os.Open(srcDir + "/" + parameters.ActionData + ".tar") + if err != nil { + log.Fatal().Err(err).Msg("Error opening tar archive") + } + // Close tar archive file at the end of this function + defer tarFile.Close() + // Create tar reader to unarchive tar archive + tarUnarchiver := tar.NewReader(tarFile) + // Loop to recursively unarchive tar file + unarchiveLoop: + for { + // Jump to next header in tar archive + header, err := tarUnarchiver.Next() + switch { + // If EOF + case err == io.EOF: + // break loop + break unarchiveLoop + case err != nil: + log.Fatal().Err(err).Msg("Error unarchiving tar archive") + // If nil header + case header == nil: + // Skip + continue + } + // Set target path to header name in destination dir + targetPath := filepath.Join(dstDir, header.Name) + switch header.Typeflag { + // If regular file + case tar.TypeReg: + // Try to create containing folder ignoring errors + _ = os.MkdirAll(filepath.Dir(targetPath), 0755) + // Create file with mode contained in header at target path + dstFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + log.Fatal().Err(err).Msg("Error creating file during unarchiving") + } + // Copy data from tar archive into file + _, err = io.Copy(dstFile, tarUnarchiver) + if err != nil { + log.Fatal().Err(err).Msg("Error copying data to file") + } + } + } + // Catchall + } else { + // Log unknown action type + log.Fatal().Msg("Unknown action type " + parameters.ActionType) + } +}