github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/distutil/copy-files.go (about)

     1  package distutil
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"regexp"
     9  	"time"
    10  )
    11  
    12  // DefaultFileInclPattern is a sensible default set of "static" files.
    13  // This might be updated from time to time to include new types of assets used on the web.
    14  // Extensions which are used for server executables, server configuration files, or files with
    15  // an empty extension will not be added here.
    16  var DefaultFileInclPattern = regexp.MustCompile(`[.](css|js|html|map|jpg|jpeg|png|gif|svg|eot|ttf|otf|woff|woff2|wasm)$`)
    17  
    18  func must(err error) {
    19  	if err != nil {
    20  		panic(err)
    21  	}
    22  }
    23  
    24  // MustCopyDirFiltered is like CopyDirFiltered but panics on error.
    25  func MustCopyDirFiltered(srcDir, dstDir string, fileInclPattern *regexp.Regexp) {
    26  	must(CopyDirFiltered(srcDir, dstDir, fileInclPattern))
    27  }
    28  
    29  // CopyDirFiltered recursively copies from srcDir to dstDir that match fileInclPattern.
    30  // fileInclPattern is only checked against the base name of the file, not its directory.
    31  // If fileInclPattern is nil, DefaultFileInclPattern will be used.
    32  // The dstDir is skipped if encountered when recursing into srcDir.
    33  // Directories are only created in the output dir if there's a file there.
    34  // dstDir must already exist.  For individual file copies, CopyFile() is used, which means
    35  // files with the same name, modification time and size are assumed to be up to date and
    36  // the function returns immediately.  Conversely when the copy succeeds the modification
    37  // time is set to that of the source.
    38  func CopyDirFiltered(srcDir, dstDir string, fileInclPattern *regexp.Regexp) error {
    39  
    40  	if fileInclPattern == nil {
    41  		fileInclPattern = DefaultFileInclPattern
    42  	}
    43  
    44  	srcDir, err := filepath.Abs(srcDir)
    45  	if err != nil {
    46  		return err
    47  	}
    48  	dstDir, err = filepath.Abs(dstDir)
    49  	if err != nil {
    50  		return err
    51  	}
    52  
    53  	var copydir func(src, dst string) error
    54  	copydir = func(src, dst string) error {
    55  
    56  		src, err := filepath.Abs(src)
    57  		if err != nil {
    58  			return err
    59  		}
    60  		if src == dstDir { // makes it so dstDir can be inside srcDir without causing problems
    61  			// fmt.Printf("skipping destination dir: %s\n", dstDir)
    62  			return nil
    63  		}
    64  		dst, err = filepath.Abs(dst)
    65  		if err != nil {
    66  			return err
    67  		}
    68  
    69  		srcf, err := os.Open(src)
    70  		if err != nil {
    71  			return err
    72  		}
    73  		defer srcf.Close()
    74  
    75  		srcFIs, err := srcf.Readdir(-1)
    76  		if err != nil {
    77  			return err
    78  		}
    79  
    80  		for _, srcFI := range srcFIs {
    81  
    82  			// for directories we recurse...
    83  			if srcFI.IsDir() {
    84  				nextSrc := filepath.Join(src, srcFI.Name())
    85  				nextDst := filepath.Join(dst, srcFI.Name())
    86  				err := copydir(nextSrc, nextDst)
    87  				if err != nil {
    88  					return err
    89  				}
    90  				continue
    91  			}
    92  
    93  			// for files...
    94  
    95  			// skip if it doesn't match the pattern
    96  			if !fileInclPattern.MatchString(srcFI.Name()) {
    97  				continue
    98  			}
    99  
   100  			// make sure the destination directory exists
   101  			err = os.MkdirAll(dst, 0755)
   102  			if err != nil {
   103  				return err
   104  			}
   105  
   106  			srcFile := filepath.Join(src, srcFI.Name())
   107  			dstFile := filepath.Join(dst, srcFI.Name())
   108  
   109  			// copy the file
   110  			err = CopyFile(srcFile, dstFile)
   111  			if err != nil {
   112  				return err
   113  			}
   114  
   115  		}
   116  
   117  		return nil
   118  	}
   119  	return copydir(srcDir, dstDir)
   120  
   121  }
   122  
   123  // MustCopyFile is like CopyFile but panics on error.
   124  func MustCopyFile(src, dst string) {
   125  	must(CopyFile(src, dst))
   126  }
   127  
   128  // CopyFile copies src to dest. Will not copy directories.  Symlinks will have their contents copied.
   129  // Files with the same name, modification time and size are assumed to be up to date and
   130  // the function returns immediately.  Conversely when the copy succeeds the modification
   131  // time is set to that of the source.
   132  func CopyFile(src, dst string) error {
   133  
   134  	src, err := filepath.Abs(src)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	dst, err = filepath.Abs(dst)
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	srcFI, err := os.Stat(src)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	dstFI, err := os.Stat(dst)
   149  	if err == nil {
   150  
   151  		if dstFI.IsDir() {
   152  			return fmt.Errorf("destination (%q) is directory, cannot CopyFile", dst)
   153  		}
   154  
   155  		// destination file exists, let's see if it looks like it's the same
   156  		dstModTime := dstFI.ModTime().Truncate(time.Second)
   157  		srcModTime := srcFI.ModTime().Truncate(time.Second)
   158  		if dstModTime == srcModTime && srcFI.Size() == dstFI.Size() {
   159  			return nil // looks like our work is already done, just return
   160  		}
   161  	}
   162  
   163  	// open the file, create if it doesn't exist, truncate if it does
   164  	dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, srcFI.Mode())
   165  	if err != nil {
   166  		return err
   167  	}
   168  	defer dstF.Close()
   169  
   170  	srcF, err := os.Open(src)
   171  	if err != nil {
   172  		return err
   173  	}
   174  	defer srcF.Close()
   175  
   176  	_, err = io.Copy(dstF, srcF)
   177  	if err != nil {
   178  		return err
   179  	}
   180  
   181  	// update destionation file's mod timestamp to match the source file
   182  	err = os.Chtimes(dst, time.Now(), srcFI.ModTime())
   183  	if err != nil {
   184  		return err
   185  	}
   186  
   187  	return nil
   188  }