├── .gitignore ├── LICENSE ├── README.md ├── README.txt ├── backup.go ├── ftp └── ftp.go ├── go.mod ├── project └── project.go └── robot └── robot.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.json 3 | dist/ 4 | test/ 5 | *.exe 6 | backups/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ONE Robotics Company 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BackupTool 2 | 3 | BackupTool is a command-line utility that allows you to quickly 4 | take backups of all robots in your project. 5 | 6 | ## Installation 7 | 8 | 1. Extract BackupTool.exe somewhere 9 | 2. [Add that directory to your system PATH](https://www.google.com/search?q=windows+7+path#q=how+to+set+windows+7+path) 10 | 11 | If you'd rather not make changes to your system's PATH, you can just 12 | extract BackupTool.exe to your current project's directory. 13 | 14 | ## Usage 15 | 16 | 1. Open a command prompt within your current project's directory 17 | 2. `BackupTool init` 18 | 3. `BackupTool add` to add each robot in your project 19 | 4. `BackupTool backup all` to backup MD:*.* 20 | 21 | Simply type `BackupTool` for help/guidance on options. 22 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | # BackupTool v1.0.0 by ONE Robotics Company 2 | 3 | ## Install 4 | 5 | 1. Extract zip file 6 | 2. Do one of the following with the appropriate version (32 or 64 bit): 7 | 1. Copy to your current project's directory and rename to whatever you like (e.g. backup.exe) 8 | 2. Extract somewhere and add that directory to your system's PATH variable 9 | 10 | ## USAGE 11 | 12 | 1. Create config file by running `BackupTool` once in your project directory 13 | 2. Use `BackupTool add` to add robots to your project 14 | 3. Use `BackupTool backup` to view a list of filters e.g. `BackupTool backup all` while grab everything off of all robots. 15 | 16 | You can always simply type `BackupTool` for a reference on usage. 17 | -------------------------------------------------------------------------------- /backup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/onerobotics/backup/project" 11 | ) 12 | 13 | var filters map[string][]string 14 | var robotNamelistFlag project.RobotNamelist 15 | 16 | func init() { 17 | filters = make(map[string][]string) 18 | filters["all"] = []string{"*.*"} 19 | filters["tp"] = []string{"*.tp"} 20 | filters["ls"] = []string{"*.ls"} 21 | filters["vr"] = []string{"*.vr"} 22 | filters["va"] = []string{"*.va"} 23 | filters["sv"] = []string{"*.sv"} 24 | filters["vision"] = []string{"*.vd", "*.vda", "*.zip"} 25 | filters["app"] = []string{"*.tp", "numreg.vr", "posreg.vr"} 26 | filters["ascii"] = []string{"*.ls", "*.va", "*.dat", "*.dg", "*.xml"} 27 | filters["bin"] = []string{"*.zip", "*.sv", "*.tp", "*.vr"} 28 | 29 | flag.Var(&robotNamelistFlag, "r", "comma-separated list of robot names") 30 | } 31 | 32 | func usage() { 33 | fmt.Println(` 34 | BackupTool v1.0.1 35 | ----------------- 36 | FANUC robot backups made easy by ONE Robotics Company. 37 | 38 | Author: Jay Strybis 39 | Website: https://www.onerobotics.com 40 | 41 | Usage: 42 | 43 | backuptool command [arguments] 44 | 45 | The commands are: 46 | 47 | add, a Add a robot to a project 48 | backup, b Perform a backup 49 | remove, r Remove a robot from a project 50 | help, h Show this screen or command-specific help`) 51 | } 52 | 53 | func addUsage() { 54 | fmt.Println(` 55 | usage: backuptool add 56 | 57 | Follow the instructions in the CLI wizard to add a robot 58 | to the current project.`) 59 | } 60 | 61 | func backupUsage() { 62 | fmt.Printf(` 63 | usage: backuptool backup [flags] filter 64 | 65 | The filters are: 66 | all *.* 67 | tp *.tp 68 | ls *.ls 69 | vr *.vr 70 | va *.va 71 | sv *.sv 72 | vision *.vd, *.vda, *.zip 73 | app *.tp, numreg.vr, posreg.vr 74 | ascii *.ls, *.va, *.dat, *.dg, *.xml 75 | bin *.zip, *.sv, *.tp, *.vr 76 | 77 | The flags are: 78 | -r comma-separated list of robot names 79 | Used to backup a subset of the project's robots 80 | 81 | `) 82 | } 83 | 84 | func removeUsage() { 85 | fmt.Println(` 86 | usage: backuptool remove 87 | 88 | Follow the CLI wizard to remove a robot from your project`) 89 | } 90 | 91 | func main() { 92 | flag.Parse() 93 | args := flag.Args() 94 | 95 | p, err := project.Init() 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | 100 | if len(args) < 1 { 101 | usage() 102 | os.Exit(0) 103 | } 104 | 105 | switch args[0] { 106 | case "add", "a": 107 | if len(args) > 1 { 108 | addUsage() 109 | os.Exit(1) 110 | } 111 | 112 | err := p.AddRobot() 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | case "backup", "b": 117 | if len(args) < 2 { 118 | backupUsage() 119 | os.Exit(1) 120 | } 121 | 122 | filter, ok := filters[args[1]] 123 | if !ok { 124 | fmt.Printf("Invalid filter: %s\n", args[1]) 125 | backupUsage() 126 | os.Exit(1) 127 | } 128 | 129 | err := p.Backup(robotNamelistFlag, func(filename string) bool { 130 | for _, f := range filter { 131 | if f[0] == '*' { 132 | return filepath.Ext(filename) == f[1:] 133 | } else { 134 | return filename == f 135 | } 136 | } 137 | 138 | return false 139 | }, args[1]) 140 | if err != nil { 141 | fmt.Println(err) 142 | os.Exit(1) 143 | } 144 | case "remove", "r": 145 | if len(args) > 1 { 146 | removeUsage() 147 | os.Exit(1) 148 | } 149 | 150 | err := p.RemoveRobot() 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | case "help", "h": 155 | if len(args) < 2 { 156 | usage() 157 | return 158 | } 159 | 160 | switch args[1] { 161 | case "add", "a": 162 | addUsage() 163 | case "backup", "b": 164 | backupUsage() 165 | case "remove", "r": 166 | removeUsage() 167 | } 168 | default: 169 | usage() 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /ftp/ftp.go: -------------------------------------------------------------------------------- 1 | package ftp 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "log" 8 | "net" 9 | "net/textproto" 10 | "os" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | func check(e error) { 18 | if e != nil { 19 | log.Fatal("FATAL:", e) 20 | } 21 | } 22 | 23 | type Connection struct { 24 | c net.Conn 25 | conn *textproto.Conn 26 | addr string 27 | port string 28 | Debug bool 29 | } 30 | 31 | func NewConnection(addr string, port string) *Connection { 32 | var c Connection 33 | c.addr = addr 34 | c.port = port 35 | return &c 36 | } 37 | 38 | func (c *Connection) debug(v ...interface{}) { 39 | if c.Debug { 40 | log.Println(v...) 41 | } 42 | } 43 | 44 | func (c *Connection) debugf(format string, v ...interface{}) { 45 | if c.Debug { 46 | log.Printf(format, v...) 47 | } 48 | } 49 | 50 | func (c *Connection) debugResponse(code int, msg string) { 51 | if c.Debug { 52 | log.Printf("code: %d, msg: %v\n", code, msg) 53 | } 54 | } 55 | 56 | func (c *Connection) Connect() error { 57 | c.debugf("Connecting to %s", c.addr+":"+c.port) 58 | conn, err := net.Dial("tcp", c.addr+":"+c.port) 59 | if err != nil { 60 | return err 61 | } 62 | c.c = conn 63 | 64 | c.conn = textproto.NewConn(conn) 65 | code, msg, err := c.conn.ReadResponse(2) 66 | c.debugResponse(code, msg) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (c *Connection) Cmd(exp int, format string, args ...interface{}) (code int, msg string, err error) { 75 | err = c.conn.PrintfLine(format, args...) 76 | if err != nil { 77 | return 78 | } 79 | 80 | return c.conn.ReadResponse(exp) 81 | } 82 | 83 | func (c *Connection) Quit() error { 84 | code, msg, err := c.Cmd(221, "QUIT") 85 | c.debugResponse(code, msg) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (c *Connection) Type(t string) error { 94 | code, msg, err := c.Cmd(200, "TYPE %s", t) 95 | c.debugResponse(code, msg) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | 103 | var passiveRegexp = regexp.MustCompile(`([\d]+),([\d]+),([\d]+),([\d]+),([\d]+),([\d]+)`) 104 | 105 | func (c *Connection) Passive() (net.Conn, error) { 106 | code, msg, err := c.Cmd(227, "PASV") 107 | c.debugResponse(code, msg) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | matches := passiveRegexp.FindStringSubmatch(msg) 113 | if matches == nil { 114 | return nil, errors.New("Cannot parse PASV response: " + msg) 115 | } 116 | 117 | ph, err := strconv.Atoi(matches[5]) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | pl, err := strconv.Atoi(matches[6]) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | port := strconv.Itoa((ph << 8) | pl) 128 | addr := strings.Join(matches[1:5], ".") + ":" + port 129 | 130 | timeout := 10 * time.Second 131 | dconn, err := net.DialTimeout("tcp", addr, timeout) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return dconn, nil 137 | } 138 | 139 | // todo: support argument to namelist e.g. *.ls 140 | func (c *Connection) NameList() ([]string, error) { 141 | dconn, err := c.Passive() 142 | if err != nil { 143 | return nil, err 144 | } 145 | defer dconn.Close() 146 | 147 | code, msg, err := c.Cmd(1, "NLST") 148 | c.debugResponse(code, msg) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | var files []string 154 | scanner := bufio.NewScanner(dconn) 155 | c.debug("Getting list of files...") 156 | for scanner.Scan() { 157 | files = append(files, scanner.Text()) 158 | } 159 | err = scanner.Err() 160 | if err != nil { 161 | return nil, err 162 | } 163 | dconn.Close() 164 | 165 | c.debugf("Received list of %d files\n", len(files)) 166 | c.debug("Waiting for response from main connection...") 167 | 168 | code, msg, err = c.conn.ReadResponse(226) 169 | c.debugResponse(code, msg) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | return files, nil 175 | } 176 | 177 | func (c *Connection) Download(filename string, dest string) error { 178 | if filename[0] == '-' { 179 | return nil 180 | } 181 | 182 | f, err := os.Create(dest + "/" + filename) 183 | if err != nil { 184 | return err 185 | } 186 | defer f.Close() 187 | 188 | w := bufio.NewWriter(f) 189 | defer w.Flush() 190 | 191 | dconn, err := c.Passive() 192 | if err != nil { 193 | return err 194 | } 195 | defer dconn.Close() 196 | 197 | code, msg, err := c.Cmd(1, "RETR %s", filename) 198 | if err != nil { 199 | return err 200 | } 201 | 202 | _, err = io.Copy(w, dconn) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | dconn.Close() 208 | 209 | code, msg, err = c.conn.ReadResponse(2) 210 | c.debugResponse(code, msg) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | return nil 216 | } 217 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/onerobotics/backup 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /project/project.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/onerobotics/backup/robot" 17 | ) 18 | 19 | const VERSION = "1.0.1" 20 | const JSON_FILENAME = "backup_tool.json" 21 | 22 | type RobotNamelist []string 23 | 24 | func (r *RobotNamelist) String() string { 25 | return fmt.Sprint(*r) 26 | } 27 | 28 | func (r *RobotNamelist) Set(value string) error { 29 | if len(*r) > 0 { 30 | return errors.New("robot namelist flag already been set") 31 | } 32 | 33 | for _, n := range strings.Split(value, ",") { 34 | *r = append(*r, n) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | type Project struct { 41 | Destination string 42 | Version string 43 | Robots []robot.Robot 44 | } 45 | 46 | func (p *Project) fromJSON() error { 47 | data, err := ioutil.ReadFile(JSON_FILENAME) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | json.Unmarshal([]byte(data), p) 53 | 54 | return nil 55 | } 56 | 57 | func (p *Project) fromWizard() error { 58 | r := bufio.NewReader(os.Stdin) 59 | 60 | questions: 61 | fmt.Println("Where should backups be stored?") 62 | dest, err := r.ReadString('\n') 63 | if err != nil { 64 | return err 65 | } 66 | dest = strings.TrimSpace(dest) 67 | 68 | confirm: 69 | fmt.Printf("Destination: %s\n", dest) 70 | fmt.Println("Is this correct? (Y/N)") 71 | 72 | answer, err := r.ReadString('\n') 73 | if err != nil { 74 | return err 75 | } 76 | 77 | answer = strings.ToLower(strings.TrimSpace(answer)) 78 | switch answer { 79 | case "y": 80 | // noop 81 | case "n": 82 | goto questions 83 | default: 84 | goto confirm 85 | } 86 | 87 | p.Destination = dest 88 | p.Version = VERSION 89 | 90 | return p.Save() 91 | } 92 | 93 | func Init() (*Project, error) { 94 | p := &Project{} 95 | 96 | err := p.fromJSON() 97 | if os.IsNotExist(err) { 98 | err = p.fromWizard() 99 | } 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return p, nil 105 | } 106 | 107 | func (p *Project) Save() error { 108 | p.Version = VERSION 109 | 110 | b, err := json.Marshal(p) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | err = ioutil.WriteFile(JSON_FILENAME, b, 0644) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | fmt.Println("Project saved.") 121 | 122 | return nil 123 | } 124 | 125 | func (p *Project) AddRobot() error { 126 | r, err := robot.FromWizard() 127 | if err != nil { 128 | return err 129 | } 130 | 131 | p.Robots = append(p.Robots, *r) 132 | return p.Save() 133 | } 134 | 135 | func (p *Project) RemoveRobot() error { 136 | if len(p.Robots) < 1 { 137 | fmt.Println("Your project does not have any robots. Please run `BackupTool add` to add one.") 138 | return nil 139 | } 140 | 141 | list: 142 | for id, robot := range p.Robots { 143 | fmt.Printf("%d. %s %s\n", id+1, robot.Name, robot.Host) 144 | } 145 | 146 | fmt.Println("\nWhich robot do you want to remove?") 147 | 148 | var id int 149 | _, err := fmt.Scanf("%d", &id) 150 | if err != nil { 151 | fmt.Println("Invalid id. Try again.") 152 | goto list 153 | } 154 | 155 | id = id - 1 156 | if id < 0 || id > len(p.Robots)-1 { 157 | fmt.Println("Id out of range") 158 | goto list 159 | } 160 | 161 | fmt.Printf("Removing robot #%d\n", id+1) 162 | 163 | p.Robots = append(p.Robots[:id], p.Robots[id+1:]...) 164 | 165 | return p.Save() 166 | } 167 | 168 | func (p *Project) filteredRobots(namelist RobotNamelist) []robot.Robot { 169 | if len(namelist) == 0 { 170 | return p.Robots 171 | } 172 | 173 | var l []robot.Robot 174 | for _, n := range namelist { 175 | for _, r := range p.Robots { 176 | if r.Name == n { 177 | l = append(l, r) 178 | } 179 | } 180 | } 181 | 182 | return l 183 | } 184 | 185 | func (p *Project) Backup(namelist RobotNamelist, filter func(string) bool, name string) error { 186 | if len(p.Robots) < 1 { 187 | return errors.New("Your project does not have any robots. Please run `BackupTool add` to add one.") 188 | } 189 | 190 | robotList := p.filteredRobots(namelist) 191 | if len(robotList) < 1 { 192 | var names []string 193 | for _, r := range p.Robots { 194 | names = append(names, r.Name) 195 | } 196 | return fmt.Errorf(`No robot names match the provided namelist: %v 197 | Available names are %v`, namelist, names) 198 | } 199 | 200 | t := time.Now() 201 | 202 | log.Println("Backing up project...") 203 | 204 | dest := filepath.Join(p.Destination, fmt.Sprintf("%d-%02d-%02dT%02d-%02d-%02d_%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), name)) 205 | 206 | var wg sync.WaitGroup 207 | for _, r := range robotList { 208 | wg.Add(1) 209 | go r.Backup(filter, dest, &wg) 210 | } 211 | wg.Wait() 212 | 213 | log.Printf("Backed up all robots in %v", time.Since(t)) 214 | 215 | return nil 216 | } 217 | -------------------------------------------------------------------------------- /robot/robot.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/onerobotics/backup/ftp" 13 | ) 14 | 15 | type Robot struct { 16 | Name string 17 | Host string 18 | } 19 | 20 | func FromWizard() (*Robot, error) { 21 | r := bufio.NewReader(os.Stdin) 22 | begin: 23 | fmt.Println("Please provide a name for the robot (e.g. R1):") 24 | name, err := r.ReadString('\n') 25 | if err != nil { 26 | return nil, err 27 | } 28 | name = strings.TrimSpace(name) 29 | 30 | fmt.Printf("What is %s's IP address?\n", name) 31 | host, err := r.ReadString('\n') 32 | if err != nil { 33 | return nil, err 34 | } 35 | host = strings.TrimSpace(host) 36 | 37 | confirm: 38 | fmt.Printf("Name: %s\nIP: %s\n", name, host) 39 | fmt.Println("Is this correct? (Y/N)") 40 | answer, err := r.ReadString('\n') 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | answer = strings.ToLower(strings.TrimSpace(answer)) 46 | switch answer { 47 | case "y": 48 | return &Robot{name, host}, nil 49 | case "n": 50 | goto begin 51 | default: 52 | goto confirm 53 | } 54 | } 55 | 56 | func (r Robot) Backup(filter func(filename string) bool, destination string, wg *sync.WaitGroup) error { 57 | defer wg.Done() 58 | 59 | t := time.Now() 60 | 61 | log.Println("Backing up", r.Name, "at", r.Host) 62 | dirname := destination + "/" + r.Name 63 | err := os.MkdirAll(dirname, os.ModePerm) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | c := ftp.NewConnection(r.Host, "21") 69 | err = c.Connect() 70 | if err != nil { 71 | return err 72 | } 73 | defer c.Quit() 74 | 75 | files, err := c.NameList() 76 | if err != nil { 77 | log.Println("error getting list of files", err) 78 | return err 79 | } 80 | 81 | err = c.Type("I") 82 | if err != nil { 83 | return err 84 | } 85 | 86 | var errorList []error 87 | for _, file := range files { 88 | if filter(file) { 89 | log.Printf("%s: Downloading %s", r.Name, file) 90 | err := c.Download(file, dirname) 91 | if err != nil { 92 | errorList = append(errorList, err) 93 | } 94 | } else { 95 | //log.Printf("%s: Skipping %s", r.Name, file) 96 | } 97 | } 98 | 99 | if len(errorList) > 0 { 100 | log.Printf("There were %d errors.\n", len(errorList)) 101 | for _, err := range errorList { 102 | log.Println(err) 103 | } 104 | } 105 | 106 | log.Printf("Finished backing up %s in %v\n", r.Name, time.Since(t)) 107 | 108 | return nil 109 | } 110 | --------------------------------------------------------------------------------