github.com/hashicorp/go-getter/v2@v2.2.2/get_smbclient.go (about)

     1  package getter
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"net/url"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"syscall"
    15  )
    16  
    17  // SmbClientGetter is a Getter implementation that will download a module from
    18  // a shared folder using smbclient cli.
    19  type SmbClientGetter struct {
    20  
    21  	// Timeout in seconds sets a deadline which all smb client CLI operations should
    22  	// complete within. Defaults to zero which means to use the default client timeout of 20 seconds.
    23  	Timeout int
    24  }
    25  
    26  func (g *SmbClientGetter) Mode(ctx context.Context, u *url.URL) (Mode, error) {
    27  	if u.Host == "" || u.Path == "" {
    28  		return 0, new(smbPathError)
    29  	}
    30  
    31  	// Use smbclient cli to verify mode
    32  	mode, err := g.smbClientMode(u)
    33  	if err == nil {
    34  		return mode, nil
    35  	}
    36  	return 0, &smbGeneralError{err}
    37  }
    38  
    39  func (g *SmbClientGetter) smbClientMode(u *url.URL) (Mode, error) {
    40  	hostPath, filePath, err := g.findHostAndFilePath(u)
    41  	if err != nil {
    42  		return 0, err
    43  	}
    44  	file := ""
    45  	// Get file and subdirectory name when existent
    46  	if strings.Contains(filePath, "/") {
    47  		i := strings.LastIndex(filePath, "/")
    48  		file = filePath[i+1:]
    49  		filePath = filePath[:i]
    50  	} else {
    51  		file = filePath
    52  		filePath = "."
    53  	}
    54  
    55  	cmdArgs := g.smbclientCmdArgs(u.User, hostPath, filePath)
    56  	// check if file exists in the smb shared folder and check the mode
    57  	isDir, err := g.isDirectory(cmdArgs, file)
    58  	if err != nil {
    59  		return 0, err
    60  	}
    61  	if isDir {
    62  		return ModeDir, nil
    63  	}
    64  	return ModeFile, nil
    65  }
    66  
    67  func (g *SmbClientGetter) Get(ctx context.Context, req *Request) error {
    68  	if req.u.Host == "" || req.u.Path == "" {
    69  		return new(smbPathError)
    70  	}
    71  
    72  	// If dst folder doesn't exists, we need to remove the created on later in case of failures
    73  	dstExisted := false
    74  	if req.Dst != "" {
    75  		if _, err := os.Lstat(req.Dst); err == nil {
    76  			dstExisted = true
    77  		}
    78  	}
    79  
    80  	// Download the directory content using smbclient cli
    81  	err := g.smbclientGet(req)
    82  	if err == nil {
    83  		return nil
    84  	}
    85  
    86  	if !dstExisted {
    87  		// Remove the destination created for smbclient
    88  		os.Remove(req.Dst)
    89  	}
    90  
    91  	return &smbGeneralError{err}
    92  }
    93  
    94  func (g *SmbClientGetter) smbclientGet(req *Request) error {
    95  	hostPath, directory, err := g.findHostAndFilePath(req.u)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	cmdArgs := g.smbclientCmdArgs(req.u.User, hostPath, ".")
   101  	// check directory exists in the smb shared folder and is a directory
   102  	isDir, err := g.isDirectory(cmdArgs, directory)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	if !isDir {
   107  		return fmt.Errorf("%s source path must be a directory", directory)
   108  	}
   109  
   110  	// download everything that's inside the directory (files and subdirectories)
   111  	cmdArgs = append(cmdArgs, "-c")
   112  	cmdArgs = append(cmdArgs, "prompt OFF;recurse ON; mget *")
   113  
   114  	if req.Dst != "" {
   115  		_, err := os.Lstat(req.Dst)
   116  		if err != nil {
   117  			if os.IsNotExist(err) {
   118  				// Create destination folder if it doesn't exist
   119  				if err := os.MkdirAll(req.Dst, 0755); err != nil {
   120  					return fmt.Errorf("failed to create destination path: %s", err.Error())
   121  				}
   122  			} else {
   123  				return err
   124  			}
   125  		}
   126  	}
   127  
   128  	_, err = g.runSmbClientCommand(req.Dst, cmdArgs)
   129  	return err
   130  }
   131  
   132  func (g *SmbClientGetter) GetFile(ctx context.Context, req *Request) error {
   133  	if req.u.Host == "" || req.u.Path == "" {
   134  		return new(smbPathError)
   135  	}
   136  
   137  	// If dst folder doesn't exist, we need to remove the created one later in case of failures
   138  	dstExisted := false
   139  	if req.Dst != "" {
   140  		if _, err := os.Lstat(req.Dst); err == nil {
   141  			dstExisted = true
   142  		}
   143  	}
   144  
   145  	// If not mounted, try downloading the file using smbclient cli
   146  	err := g.smbclientGetFile(req)
   147  	if err == nil {
   148  		return nil
   149  	}
   150  
   151  	if !dstExisted {
   152  		// Remove the destination created for smbclient
   153  		os.Remove(req.Dst)
   154  	}
   155  
   156  	return &smbGeneralError{err}
   157  }
   158  
   159  func (g *SmbClientGetter) smbclientGetFile(req *Request) error {
   160  	hostPath, filePath, err := g.findHostAndFilePath(req.u)
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	// Get file and subdirectory name when existent
   166  	file := ""
   167  	if strings.Contains(filePath, "/") {
   168  		i := strings.LastIndex(filePath, "/")
   169  		file = filePath[i+1:]
   170  		filePath = filePath[:i]
   171  	} else {
   172  		file = filePath
   173  		filePath = "."
   174  	}
   175  
   176  	cmdArgs := g.smbclientCmdArgs(req.u.User, hostPath, filePath)
   177  	// check file exists in the smb shared folder and is not a directory
   178  	isDir, err := g.isDirectory(cmdArgs, file)
   179  	if err != nil {
   180  		return err
   181  	}
   182  	if isDir {
   183  		return fmt.Errorf("%s source path must be a file", file)
   184  	}
   185  
   186  	// download file
   187  	cmdArgs = append(cmdArgs, "-c")
   188  	if req.Dst != "" {
   189  		_, err := os.Lstat(req.Dst)
   190  		if err != nil {
   191  			if os.IsNotExist(err) {
   192  				// Create destination folder if it doesn't exist
   193  				if err := os.MkdirAll(filepath.Dir(req.Dst), 0755); err != nil {
   194  					return fmt.Errorf("failed to creat destination path: %s", err.Error())
   195  				}
   196  			} else {
   197  				return err
   198  			}
   199  		}
   200  		cmdArgs = append(cmdArgs, fmt.Sprintf("get %s %s", file, req.Dst))
   201  	} else {
   202  		cmdArgs = append(cmdArgs, fmt.Sprintf("get %s", file))
   203  	}
   204  
   205  	_, err = g.runSmbClientCommand("", cmdArgs)
   206  	return err
   207  }
   208  
   209  func (g *SmbClientGetter) smbclientCmdArgs(used *url.Userinfo, hostPath string, fileDir string) (baseCmd []string) {
   210  	baseCmd = append(baseCmd, "-N")
   211  
   212  	// Append auth user and password to baseCmd
   213  	auth := used.Username()
   214  	if auth != "" {
   215  		if password, ok := used.Password(); ok {
   216  			auth = auth + "%" + password
   217  		}
   218  		baseCmd = append(baseCmd, "-U")
   219  		baseCmd = append(baseCmd, auth)
   220  	}
   221  
   222  	baseCmd = append(baseCmd, hostPath)
   223  	baseCmd = append(baseCmd, "--directory")
   224  	baseCmd = append(baseCmd, fileDir)
   225  	if g.Timeout > 0 {
   226  		baseCmd = append(baseCmd, "-t")
   227  		baseCmd = append(baseCmd, strconv.Itoa(g.Timeout))
   228  	}
   229  	return baseCmd
   230  }
   231  
   232  func (g *SmbClientGetter) findHostAndFilePath(u *url.URL) (string, string, error) {
   233  	// Host path
   234  	hostPath := "//" + u.Host
   235  
   236  	// Get shared directory
   237  	path := strings.TrimPrefix(u.Path, "/")
   238  	splt := regexp.MustCompile(`/`)
   239  	directories := splt.Split(path, 2)
   240  
   241  	if len(directories) > 0 {
   242  		hostPath = hostPath + "/" + directories[0]
   243  	}
   244  
   245  	// Check file path
   246  	if len(directories) <= 1 || directories[1] == "" {
   247  		return "", "", fmt.Errorf("can not find file path and/or name in the smb url")
   248  	}
   249  
   250  	return hostPath, directories[1], nil
   251  }
   252  
   253  func (g *SmbClientGetter) isDirectory(args []string, object string) (bool, error) {
   254  	args = append(args, "-c")
   255  	args = append(args, fmt.Sprintf("allinfo %s", object))
   256  	output, err := g.runSmbClientCommand("", args)
   257  	if err != nil {
   258  		return false, err
   259  	}
   260  	if strings.Contains(output, "OBJECT_NAME_NOT_FOUND") {
   261  		return false, fmt.Errorf("source path not found: %s", output)
   262  	}
   263  	return strings.Contains(output, "attributes: D"), nil
   264  }
   265  
   266  func (g *SmbClientGetter) runSmbClientCommand(dst string, args []string) (string, error) {
   267  	ctx := context.Background()
   268  	cmd := exec.CommandContext(ctx, "smbclient", args...)
   269  
   270  	if dst != "" {
   271  		cmd.Dir = dst
   272  	}
   273  
   274  	var buf bytes.Buffer
   275  	cmd.Stdout = &buf
   276  	cmd.Stderr = &buf
   277  
   278  	err := cmd.Run()
   279  	if err == nil {
   280  		return buf.String(), nil
   281  	}
   282  	if exiterr, ok := err.(*exec.ExitError); ok {
   283  		// The program has exited with an exit code != 0
   284  		if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   285  			return buf.String(), fmt.Errorf(
   286  				"%s exited with %d: %s",
   287  				cmd.Path,
   288  				status.ExitStatus(),
   289  				buf.String())
   290  		}
   291  	}
   292  	return buf.String(), fmt.Errorf("error running %s: %s", cmd.Path, buf.String())
   293  }
   294  
   295  func (g *SmbClientGetter) Detect(req *Request) (bool, error) {
   296  	if len(req.Src) == 0 {
   297  		return false, nil
   298  	}
   299  
   300  	if req.Forced != "" {
   301  		// There's a getter being Forced
   302  		if !g.validScheme(req.Forced) {
   303  			// Current getter is not the Forced one
   304  			// Don't use it to try to download the artifact
   305  			return false, nil
   306  		}
   307  	}
   308  	isForcedGetter := req.Forced != "" && g.validScheme(req.Forced)
   309  
   310  	u, err := url.Parse(req.Src)
   311  	if err == nil && u.Scheme != "" {
   312  		if isForcedGetter {
   313  			// Is the Forced getter and source is a valid url
   314  			return true, nil
   315  		}
   316  		if g.validScheme(u.Scheme) {
   317  			return true, nil
   318  		}
   319  		// Valid url with a scheme that is not valid for current getter
   320  		return false, nil
   321  	}
   322  
   323  	return false, nil
   324  }
   325  
   326  func (g *SmbClientGetter) validScheme(scheme string) bool {
   327  	return scheme == "smb"
   328  }
   329  
   330  type smbPathError struct {
   331  	Path string
   332  }
   333  
   334  func (e *smbPathError) Error() string {
   335  	if e.Path == "" {
   336  		return "samba path should contain valid host, filepath, and authentication if necessary (smb://<user>:<password>@<host>/<file_path>)"
   337  	}
   338  	return fmt.Sprintf("samba path should contain valid host, filepath, and authentication if necessary (%s)", e.Path)
   339  }
   340  
   341  type smbGeneralError struct {
   342  	err error
   343  }
   344  
   345  func (e *smbGeneralError) Error() string {
   346  	if e != nil {
   347  		return fmt.Sprintf("smbclient cli needs to be installed and credentials provided when necessary. \n err: %s", e.err.Error())
   348  	}
   349  	return "smbclient cli needs to be installed and credentials provided when necessary."
   350  }