github.com/jpbruinsslot/dot@v0.0.0-20231229172555-797f05ba0642/file.go (about)

     1  // file.go will hold all the operations that have to do with
     2  // file management.
     3  
     4  package main
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"log"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"strings"
    16  )
    17  
    18  func SyncFiles() {
    19  	PrintHeader("Syncing files ...")
    20  
    21  	// load config
    22  	c, err := NewConfig(PathDotConfig)
    23  	if err != nil {
    24  		PrintBodyError(
    25  			"not able to load config file. Make sure the .dotconfig file is present and points to the correct location",
    26  		)
    27  		return
    28  	}
    29  
    30  	// when we have files the sync them
    31  	if len(c.Files) > 0 {
    32  		// for every file track it
    33  		copyAll := false
    34  		for name, path := range c.Files {
    35  			// get full path
    36  			fullPath := fmt.Sprintf("%s%s", HomeDir(), path)
    37  			copyAll = TrackFile(name, fullPath, false, copyAll)
    38  		}
    39  	} else {
    40  		PrintBodyError("there aren't any files being tracked. Begin doing so with: `dot add -name [name] -path [path]`")
    41  	}
    42  }
    43  
    44  // TrackFile will track an individual file, meaning, it will move the original
    45  // file to either the files or backup folder. It will the create a symlink of
    46  // the file in the original location. `name` will be used as the name of the
    47  // folder and key in the config file. `fullPath` has to be the absolute path
    48  // to the file to be tracked.
    49  //
    50  // TrackFile can be called from two contexes:
    51  //
    52  //  1. From SyncFiles, it will read all the tracked files from the config and
    53  //     make track files if necessary.
    54  //
    55  //  2. From CommandAdd, this will add a new file for tracking
    56  //
    57  // TrackFile will make a distinction between a new file and a file that is
    58  // already been tracked:
    59  //
    60  //  1. TrackFile can't find the symlink, but the file is present in the
    61  //     dot_path (the folder that holds all the original files). Then we need to
    62  //     relink it, thus creating a symlink at the correct location. This happens
    63  //     we you run dot on a new 'additional machine'.
    64  //
    65  //  2. TrackFile can't find the symlink, and the file is also not present in
    66  //     the dot_path folder. This will mean that it is a new file were are going
    67  //     to track. So we copy the file to the files folder, create a symlink, and
    68  //     add an entry to the config file.
    69  func TrackFile(name string, fullPath string, push bool, copyAll bool) bool {
    70  	// load config
    71  	c, err := NewConfig(PathDotConfig)
    72  	if err != nil {
    73  		PrintBodyError("not able to find .dotconfig")
    74  		return copyAll
    75  	}
    76  
    77  	// Base
    78  	base := path.Base(fullPath)
    79  
    80  	// get relative path
    81  	relPath, err := GetRelativePath(fullPath)
    82  	if err != nil {
    83  		PrintBodyError(err.Error())
    84  		return copyAll
    85  	}
    86  
    87  	// check if path is present
    88  	_, err = os.Stat(fullPath)
    89  	if err != nil {
    90  		PrintBodyError(fmt.Sprintf("file not present on system: %s", fullPath))
    91  
    92  		if copyAll {
    93  			src := fmt.Sprintf("%s%s/files/%s/%s", HomeDir(), c.DotPath, name, base)
    94  			MakeAndCopyToDir(src, fullPath)
    95  			return TrackFile(name, fullPath, push, copyAll)
    96  		}
    97  
    98  		PrintBody("Copy file(s) to its destination? [All/Y/N]")
    99  		var input string
   100  		_, err := fmt.Scan(&input)
   101  		if err != nil {
   102  			log.Fatal(err)
   103  		}
   104  
   105  		switch input {
   106  		case "All":
   107  			copyAll = true
   108  			src := fmt.Sprintf("%s%s/files/%s/%s", HomeDir(), c.DotPath, name, base)
   109  			err := MakeAndCopyToDir(src, fullPath)
   110  			if err != nil {
   111  				PrintBodyError(err.Error())
   112  				return copyAll
   113  			}
   114  			return TrackFile(name, fullPath, push, copyAll)
   115  		case "Y":
   116  			src := fmt.Sprintf("%s%s/files/%s/%s", HomeDir(), c.DotPath, name, base)
   117  			err := MakeAndCopyToDir(src, fullPath)
   118  			if err != nil {
   119  				PrintBodyError(err.Error())
   120  				return copyAll
   121  			}
   122  			return TrackFile(name, fullPath, push, copyAll)
   123  		case "N":
   124  			PrintBodyError(fmt.Sprintf("Ignoring %s", name))
   125  			return copyAll
   126  		default:
   127  			PrintBodyError("Invalid input")
   128  			return copyAll
   129  		}
   130  	}
   131  
   132  	// check if path is already symlinked
   133  	s, err := os.Lstat(fullPath)
   134  	if err != nil {
   135  		return copyAll
   136  	}
   137  
   138  	if s.Mode()&os.ModeSymlink == os.ModeSymlink {
   139  		PrintBody(fmt.Sprintf("%s is already symlinked", name))
   140  		return copyAll
   141  	}
   142  
   143  	repoPath := fmt.Sprintf("%s%s/files/%s/", HomeDir(), c.DotPath, name)
   144  	if _, err := os.Stat(repoPath); err == nil {
   145  		// no symlink found, already in repo => additional machine
   146  		PrintBody(fmt.Sprintf("Symlinking: %s", name))
   147  
   148  		// put in backup folder, set named folder based on `name`, e.g.:
   149  		// `/home/jpbruinsslot/dotfiles/backup/[name]/[base]`
   150  		dst := fmt.Sprintf("%s%s/backup/%s/%s", HomeDir(), c.DotPath, name, base)
   151  		err = MakeAndMoveToDir(fullPath, dst)
   152  		if err != nil {
   153  			msg := fmt.Sprintf("not able to move files to %s (%s)", dst, err)
   154  			PrintBodyError(msg)
   155  
   156  			prompt := fmt.Sprintf("Remove %s ? [Y/N]", dst)
   157  			PrintBody(prompt)
   158  			var input string
   159  			_, err := fmt.Scan(&input)
   160  			if err != nil {
   161  				log.Fatal(err)
   162  			}
   163  
   164  			if input == "Y" {
   165  				err := os.RemoveAll(dst)
   166  				if err != nil {
   167  					log.Fatal(err)
   168  				}
   169  
   170  				err = MakeAndMoveToDir(fullPath, dst)
   171  				if err != nil {
   172  					log.Fatal(err)
   173  				}
   174  			} else {
   175  				msg := fmt.Sprintf("ignoring %s", name)
   176  				PrintBodyError(msg)
   177  				return copyAll
   178  			}
   179  		}
   180  
   181  		// trim potential trailing slash for symlink
   182  		fullPath = strings.TrimRight(fullPath, "/")
   183  
   184  		// create symlink (os.Symlink(oldname, newname))
   185  		dst = fmt.Sprintf("%s%s/files/%s/%s", HomeDir(), c.DotPath, name, base)
   186  		err = os.Symlink(dst, fullPath)
   187  		if err != nil {
   188  			log.Fatal(err)
   189  		}
   190  
   191  	} else {
   192  		// no symlink found, not in repo => new entry
   193  		PrintBody(fmt.Sprintf("Symlinking: %s", name))
   194  
   195  		// put in files folder, set named folder based on `name`, e.g.:
   196  		// `/home/jpbruinsslot/dotfiles/files/[name]/[base]`
   197  		dst := fmt.Sprintf("%s%s/files/%s/%s", HomeDir(), c.DotPath, name, base)
   198  		err = MakeAndMoveToDir(fullPath, dst)
   199  		if err != nil {
   200  			log.Fatal(err)
   201  		}
   202  
   203  		// trim potential trailing slash for symlink
   204  		fullPath = strings.TrimRight(fullPath, "/")
   205  
   206  		// create symlink (os.Symlink(oldname, newname))
   207  		err = os.Symlink(dst, fullPath)
   208  		if err != nil {
   209  			log.Fatal(err)
   210  		}
   211  
   212  		// create entry in .dotconfig file
   213  		c.Files[name] = relPath
   214  		c.Save()
   215  
   216  		// push changes to repository
   217  		if push {
   218  			GitCommitPush(name, "add")
   219  		}
   220  	}
   221  
   222  	return copyAll
   223  }
   224  
   225  // UntrackFile will remove a file from tracking. `name` will be the key
   226  // in the config file that points to the initial location of the file
   227  func UntrackFile(name string, push bool) {
   228  	// open config file
   229  	c, err := NewConfig(fmt.Sprintf("%s/%s", HomeDir(), ConfigFileName))
   230  	if err != nil {
   231  		PrintBodyError("not able to find .dotconfig")
   232  		return
   233  	}
   234  
   235  	// check if `name` is present in c.Files
   236  	path := c.Files[name]
   237  	if path == "" {
   238  		PrintBodyError(
   239  			fmt.Sprintf(
   240  				"'%s' is not being tracked. Get the list of tracked files with `dot list`",
   241  				name,
   242  			),
   243  		)
   244  		return
   245  	}
   246  
   247  	// check if path (the symlink) is present
   248  	pathSymlink := fmt.Sprintf("%s%s", HomeDir(), path)
   249  	f, err := os.Lstat(pathSymlink)
   250  	if err != nil {
   251  		PrintBodyError(fmt.Sprintf("not able to find: %s", path))
   252  		return
   253  	}
   254  
   255  	// check if path is symlink
   256  	if f.Mode()&os.ModeSymlink != os.ModeSymlink {
   257  		PrintBodyError(fmt.Sprintf("%s is not a symlink", path))
   258  		return
   259  	}
   260  
   261  	// check if src is present
   262  	src := fmt.Sprintf("%s%s/files/%s%s", HomeDir(), c.DotPath, name, path)
   263  	if _, err = os.Stat(src); err != nil {
   264  		PrintBodyError(fmt.Sprintf("not able to find %s", src))
   265  		return
   266  	}
   267  
   268  	// remove symlink
   269  	err = os.Remove(pathSymlink)
   270  	if err != nil {
   271  		PrintBodyError(fmt.Sprintf("not able to remove %s", pathSymlink))
   272  		return
   273  	}
   274  
   275  	// move the file or directory
   276  	dst := fmt.Sprintf("%s%s", HomeDir(), path)
   277  
   278  	PrintBody(fmt.Sprintf("Moving %s back to %s", name, dst))
   279  
   280  	err = MakeAndCopyToDir(src, dst)
   281  	if err != nil {
   282  		log.Fatal(err)
   283  	}
   284  
   285  	// remove tracked files from repo dir
   286  	entry := fmt.Sprintf("%s%s/files/%s", HomeDir(), c.DotPath, name)
   287  	err = os.RemoveAll(entry)
   288  	if err != nil {
   289  		log.Fatal(err)
   290  	}
   291  
   292  	// remove entry from config and save config
   293  	delete(c.Files, name)
   294  	c.Save()
   295  
   296  	// push changes to repository
   297  	if push {
   298  		GitCommitPush(name, "rm")
   299  	}
   300  }
   301  
   302  // MakeAndMoveToDir will move the source file/folder `src` to the destination
   303  // `dst` (`dst` will be absolute path to the destination).
   304  func MakeAndMoveToDir(src string, dst string) error {
   305  	err := MakeAndCopyToDir(src, dst)
   306  	if err != nil {
   307  		return err
   308  	}
   309  
   310  	err = os.RemoveAll(src)
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  func MakeAndCopyToDir(src string, dst string) error {
   319  	// folder or file
   320  	f, err := os.Stat(src)
   321  	if err != nil {
   322  		return err
   323  	}
   324  
   325  	if f.IsDir() {
   326  		err = CopyDir(src, dst)
   327  		if err != nil {
   328  			return err
   329  		}
   330  	} else {
   331  		// get directory
   332  		dir, _ := filepath.Split(dst)
   333  
   334  		// create destination dir
   335  		// err = os.MkdirAll(dir, f.Mode())
   336  		err = os.MkdirAll(dir, 0755)
   337  		if err != nil {
   338  			return err
   339  		}
   340  
   341  		// rename the file
   342  		err = CopyFile(src, dst)
   343  		if err != nil {
   344  			return err
   345  		}
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  func CopyDir(src string, dst string) error {
   352  	src = filepath.Clean(src)
   353  	dst = filepath.Clean(dst)
   354  
   355  	// Check src
   356  	f, err := os.Stat(src)
   357  	if err != nil {
   358  		return err
   359  	}
   360  
   361  	if !f.IsDir() {
   362  		return errors.New("source is not a directory")
   363  	}
   364  
   365  	// Check dst
   366  	_, err = os.Stat(dst)
   367  	if err != nil && !os.IsNotExist(err) {
   368  		return nil
   369  	}
   370  	if err == nil {
   371  		return errors.New("dst already exist")
   372  	}
   373  
   374  	files, err := ioutil.ReadDir(src)
   375  	if err != nil {
   376  		return err
   377  	}
   378  
   379  	os.MkdirAll(dst, f.Mode())
   380  	if err != nil {
   381  		return err
   382  	}
   383  
   384  	for _, file := range files {
   385  		srcPath := filepath.Join(src, file.Name())
   386  		dstPath := filepath.Join(dst, file.Name())
   387  
   388  		if file.IsDir() {
   389  			err = CopyDir(srcPath, dstPath)
   390  			if err != nil {
   391  				return err
   392  			}
   393  		} else {
   394  			if file.Mode()&os.ModeSymlink != 0 {
   395  				continue
   396  			}
   397  
   398  			err = CopyFile(srcPath, dstPath)
   399  			if err != nil {
   400  				return err
   401  			}
   402  		}
   403  	}
   404  
   405  	return nil
   406  }
   407  
   408  func CopyFile(src string, dst string) error {
   409  	inFile, err := os.Open(src)
   410  	if err != nil {
   411  		return err
   412  	}
   413  	defer inFile.Close()
   414  
   415  	outFile, err := os.Create(dst)
   416  	if err != nil {
   417  		return err
   418  	}
   419  	defer outFile.Close()
   420  
   421  	_, err = io.Copy(outFile, inFile)
   422  	if err != nil {
   423  		return err
   424  	}
   425  
   426  	err = outFile.Sync()
   427  	if err != nil {
   428  		return err
   429  	}
   430  
   431  	f, err := os.Stat(src)
   432  	if err != nil {
   433  		return err
   434  	}
   435  
   436  	err = os.Chmod(dst, f.Mode())
   437  	if err != nil {
   438  		return err
   439  	}
   440  
   441  	return nil
   442  }