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 }