/* Copyright © 2021 Arsen Musayelyan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "archive/tar" "io" "io/ioutil" "net/url" "os" "path/filepath" "strconv" "strings" "github.com/pkg/browser" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/vmihailenco/msgpack/v5" ) // 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.msgpack") 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 MessagePackData, err := msgpack.Marshal(parameters) if err != nil { log.Fatal().Err(err).Msg("Error encoding MessagePack") } // Write []byte to previously created parameters file bytesWritten, err := configFile.Write(MessagePackData) if err != nil { log.Fatal().Err(err).Msg("Error writing MessagePack to file") } // Log bytes written log.Info().Str("file", "parameters.msgpack").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 MessagePack into parameters struct err = msgpack.Unmarshal(fileData, parameters) if err != nil { log.Fatal().Err(err).Msg("Error decoding MessagePack") } } // 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 switch parameters.ActionType { case "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 case "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 case "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() // If EOF if err == io.EOF { // break loop break unarchiveLoop } else if err != nil { log.Fatal().Err(err).Msg("Error unarchiving tar archive") // If nil header } else if 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 default: // Log unknown action type log.Fatal().Msg("Unknown action type " + parameters.ActionType) } }