├── README.md ├── data └── screenshot.gif └── davc.go /README.md: -------------------------------------------------------------------------------- 1 | # davc 2 | 3 | WebDAV client 4 | 5 | ![](https://raw.githubusercontent.com/mattn/davc/master/data/screenshot.gif) 6 | 7 | ## Usage 8 | 9 | ``` 10 | $ davc http://example.com/ 11 | ``` 12 | 13 | ## Installation 14 | 15 | ``` 16 | $ go get github.com/mattn/davc 17 | ``` 18 | 19 | ## License 20 | 21 | MIT 22 | 23 | ## Author 24 | 25 | Yasuhiro Matsumoto (a.k.a mattn) 26 | -------------------------------------------------------------------------------- /data/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/davc/63ecfb0fcea990fbd494bc7a00106ac875ce4fb3/data/screenshot.gif -------------------------------------------------------------------------------- /davc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/url" 12 | "os" 13 | "os/exec" 14 | "path" 15 | "path/filepath" 16 | "strings" 17 | "time" 18 | 19 | "github.com/fatih/color" 20 | "github.com/mattn/go-runewidth" 21 | "github.com/mattn/go-shellwords" 22 | "github.com/peterh/liner" 23 | "github.com/studio-b12/gowebdav" 24 | ) 25 | 26 | var invalidArg = errors.New("invalid argument") 27 | 28 | var ( 29 | cred = flag.String("cred", os.Getenv("DAVC_CRED"), "credential for basic auth (user:password)") 30 | prompthere = flag.Bool("prompthere", false, "display location at prompt") 31 | ) 32 | 33 | func fatalRequiredAuth(err error) { 34 | fmt.Fprintln(os.Stderr, os.Args[0]+":", err) 35 | os.Exit(2) 36 | } 37 | 38 | func fatal(err error) { 39 | fmt.Fprintln(os.Stderr, os.Args[0]+":", err) 40 | os.Exit(1) 41 | } 42 | 43 | var esc = strings.NewReplacer( 44 | `\`, `\\`, 45 | ` `, `\ `, 46 | ) 47 | 48 | func escape(s string) string { 49 | return esc.Replace(s) 50 | } 51 | 52 | func parseArgs(args []string) (opts map[string]bool, retargs []string) { 53 | opts = map[string]bool{} 54 | for _, arg := range args { 55 | if strings.HasPrefix(arg, "-") { 56 | opts[arg[1:]] = true 57 | } else { 58 | retargs = append(retargs, arg) 59 | } 60 | } 61 | return 62 | } 63 | 64 | func handle(client *gowebdav.Client, cwd *string, args []string) error { 65 | lwd, err := os.Getwd() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | switch args[0] { 71 | case "lpwd": 72 | if len(args) != 1 { 73 | return invalidArg 74 | } 75 | fmt.Println(lwd) 76 | case "pwd": 77 | if len(args) != 1 { 78 | return invalidArg 79 | } 80 | fmt.Println(*cwd) 81 | case "lcd": 82 | if len(args) != 2 { 83 | return invalidArg 84 | } 85 | p := args[1] 86 | if !filepath.IsAbs(p) { 87 | p = filepath.Join(lwd, p) 88 | } 89 | fi, err := os.Stat(p) 90 | if err != nil { 91 | return err 92 | } 93 | if !fi.IsDir() { 94 | return os.ErrNotExist 95 | } 96 | err = os.Chdir(filepath.Clean(p)) 97 | if err != nil { 98 | return err 99 | } 100 | case "cd": 101 | if len(args) != 2 { 102 | return invalidArg 103 | } 104 | p := args[1] 105 | if !path.IsAbs(p) { 106 | p = path.Join(*cwd, p) 107 | } 108 | if !strings.HasSuffix(p, "/") { 109 | p += "/" 110 | } 111 | fi, err := client.Stat(p) 112 | if err != nil { 113 | return err 114 | } 115 | if !fi.IsDir() { 116 | return os.ErrNotExist 117 | } 118 | *cwd = path.Clean(p) 119 | if !strings.HasSuffix(*cwd, "/") { 120 | *cwd += "/" 121 | } 122 | case "lmkdir": 123 | if len(args) != 2 { 124 | return invalidArg 125 | } 126 | p := args[1] 127 | if !filepath.IsAbs(p) { 128 | p = filepath.Join(lwd, p) 129 | } 130 | err := os.MkdirAll(p, 0755) 131 | if err != nil { 132 | return err 133 | } 134 | case "mkdir": 135 | if len(args) != 2 { 136 | return invalidArg 137 | } 138 | p := args[1] 139 | if !path.IsAbs(p) { 140 | p = path.Join(*cwd, p) 141 | } 142 | err := client.MkdirAll(p, 0755) 143 | if err != nil { 144 | return err 145 | } 146 | case "lls": 147 | if len(args) != 1 { 148 | return invalidArg 149 | } 150 | f, err := os.Open(lwd) 151 | if err != nil { 152 | return nil 153 | } 154 | defer f.Close() 155 | fis, err := f.Readdir(0) 156 | if err != nil { 157 | return nil 158 | } 159 | for _, fi := range fis { 160 | if fi.IsDir() { 161 | fmt.Fprintln(color.Output, color.GreenString("%v", fi.Name()+"/")) 162 | } else { 163 | fmt.Fprintln(color.Output, fi.Name()) 164 | } 165 | } 166 | case "ls": 167 | var target string 168 | var opts map[string]bool 169 | var jsonout bool 170 | 171 | opts, args = parseArgs(args) 172 | jsonout = opts["json"] 173 | if len(args) == 1 { 174 | target = *cwd 175 | } else if len(args) == 2 { 176 | target = args[1] 177 | if !path.IsAbs(target) { 178 | target = path.Join(*cwd, target) 179 | } 180 | } else { 181 | return invalidArg 182 | } 183 | target = path.Clean(target) 184 | fis, err := client.ReadDir(target) 185 | if err != nil { 186 | return err 187 | } 188 | if jsonout { 189 | type fit struct { 190 | Name string `json:"name"` 191 | Size int64 `json:"size"` 192 | Mode string `json:"mode"` 193 | ModTime time.Time `json:"modtime"` 194 | IsDir bool `json:"isdir"` 195 | } 196 | ffs := make([]fit, len(fis)) 197 | for i, fi := range fis { 198 | ffs[i].Name = fi.Name() 199 | ffs[i].Size = fi.Size() 200 | ffs[i].Mode = fi.Mode().String() 201 | ffs[i].ModTime = fi.ModTime() 202 | ffs[i].IsDir = fi.IsDir() 203 | } 204 | json.NewEncoder(color.Output).Encode(ffs) 205 | } else { 206 | for _, fi := range fis { 207 | if fi.IsDir() { 208 | fmt.Fprintln(color.Output, color.GreenString("%v", fi.Name()+"/")) 209 | } else { 210 | fmt.Fprintln(color.Output, fi.Name()) 211 | } 212 | } 213 | } 214 | case "ll": 215 | if len(args) != 1 { 216 | return invalidArg 217 | } 218 | fis, err := client.ReadDir(*cwd) 219 | if err != nil { 220 | return err 221 | } 222 | for _, fi := range fis { 223 | if fi.IsDir() { 224 | fmt.Fprintln(color.Output, 225 | color.GreenString(runewidth.Truncate(fmt.Sprintf("%-20s", fi.Name()+"/"), 20, ""))+"\t"+ 226 | fmt.Sprintf("%20d", fi.Size())+"\t"+ 227 | fi.ModTime().String()) 228 | } else { 229 | fmt.Fprintln(color.Output, 230 | runewidth.Truncate(fmt.Sprintf("%-20s", fi.Name()), 20, "")+"\t"+ 231 | fmt.Sprintf("%20d", fi.Size())+"\t"+ 232 | fi.ModTime().String()) 233 | } 234 | } 235 | case "lrm": 236 | if len(args) != 2 { 237 | return invalidArg 238 | } 239 | p := args[1] 240 | if !filepath.IsAbs(p) { 241 | p = filepath.Join(lwd, p) 242 | } 243 | err := os.Remove(p) 244 | if err != nil { 245 | return err 246 | } 247 | case "rm": 248 | if len(args) != 2 { 249 | return invalidArg 250 | } 251 | p := args[1] 252 | if !path.IsAbs(p) { 253 | p = path.Join(*cwd, p) 254 | } 255 | err := client.Remove(p) 256 | if err != nil { 257 | return err 258 | } 259 | case "lrmdir": 260 | if len(args) != 2 { 261 | return invalidArg 262 | } 263 | p := args[1] 264 | if !filepath.IsAbs(p) { 265 | p = filepath.Join(lwd, p) 266 | } 267 | err := os.RemoveAll(p) 268 | if err != nil { 269 | return err 270 | } 271 | case "rmdir": 272 | if len(args) != 2 { 273 | return invalidArg 274 | } 275 | p := args[1] 276 | if !path.IsAbs(p) { 277 | p = path.Join(*cwd, p) 278 | } 279 | err := client.Remove(p) 280 | if err != nil { 281 | return err 282 | } 283 | case "put": 284 | if len(args) != 2 { 285 | return invalidArg 286 | } 287 | p := args[1] 288 | if !filepath.IsAbs(p) { 289 | p = filepath.Join(lwd, p) 290 | } 291 | f, err := os.Open(p) 292 | if err != nil { 293 | return err 294 | } 295 | _, file := filepath.Split(p) 296 | file = path.Join(*cwd, file) 297 | err = client.WriteStream(file, f, 0644) 298 | if err != nil { 299 | return err 300 | } 301 | case "get": 302 | if len(args) != 2 { 303 | return invalidArg 304 | } 305 | p := args[1] 306 | if !path.IsAbs(p) { 307 | p = path.Join(*cwd, p) 308 | } 309 | _, file := path.Split(p) 310 | strm, err := client.ReadStream(p) 311 | if err != nil { 312 | return err 313 | } 314 | defer strm.Close() 315 | f, err := os.Create(file) 316 | if err != nil { 317 | return err 318 | } 319 | defer f.Close() 320 | _, err = io.Copy(f, strm) 321 | if err == io.ErrUnexpectedEOF { 322 | return nil 323 | } 324 | return err 325 | case "cp": 326 | if len(args) != 3 { 327 | return invalidArg 328 | } 329 | src := args[1] 330 | if !path.IsAbs(src) { 331 | src = path.Join(*cwd, src) 332 | } 333 | dst := args[2] 334 | if !path.IsAbs(dst) { 335 | dst = path.Join(*cwd, dst) 336 | } 337 | err := client.Copy(src, dst, true) 338 | if err != nil { 339 | return err 340 | } 341 | case "mv": 342 | if len(args) != 3 { 343 | return invalidArg 344 | } 345 | src := args[1] 346 | if !path.IsAbs(src) { 347 | src = path.Join(*cwd, src) 348 | } 349 | dst := args[2] 350 | if !path.IsAbs(dst) { 351 | dst = path.Join(*cwd, dst) 352 | } 353 | err := client.Rename(src, dst, true) 354 | if err != nil { 355 | return err 356 | } 357 | case "cat": 358 | if len(args) != 2 { 359 | return invalidArg 360 | } 361 | p := args[1] 362 | if !path.IsAbs(p) { 363 | p = path.Join(*cwd, p) 364 | } 365 | strm, err := client.ReadStream(p) 366 | if err != nil { 367 | return err 368 | } 369 | defer strm.Close() 370 | _, err = io.Copy(os.Stdout, strm) 371 | if err == io.ErrUnexpectedEOF { 372 | err = nil 373 | } 374 | return err 375 | case "write": 376 | if len(args) != 2 { 377 | return invalidArg 378 | } 379 | p := args[1] 380 | if !path.IsAbs(p) { 381 | p = path.Join(*cwd, p) 382 | } 383 | buf := bufio.NewReader(os.Stdin) 384 | err = client.WriteStream(p, buf, 0644) 385 | if err != nil { 386 | return err 387 | } 388 | return err 389 | case "edit", "vim": 390 | if len(args) != 2 { 391 | return invalidArg 392 | } 393 | p := args[1] 394 | if !path.IsAbs(p) { 395 | p = path.Join(*cwd, p) 396 | } 397 | strm, err := client.ReadStream(p) 398 | if err != nil { 399 | return err 400 | } 401 | defer strm.Close() 402 | f, err := ioutil.TempFile("", "davc") 403 | if err != nil { 404 | return err 405 | } 406 | defer os.Remove(f.Name()) 407 | _, err = io.Copy(f, strm) 408 | f.Close() 409 | if err != nil { //&& err != io.ErrUnexpectedEOF { 410 | return err 411 | } 412 | fi, err := os.Stat(f.Name()) 413 | if err != nil { 414 | return err 415 | } 416 | editor := os.Getenv("EDITOR") 417 | if args[0] == "vim" { 418 | editor = "vim" 419 | } 420 | cmd := exec.Command(editor, f.Name()) 421 | cmd.Stdin = os.Stdin 422 | cmd.Stdout = os.Stdout 423 | cmd.Stderr = os.Stderr 424 | err = cmd.Run() 425 | if err != nil { 426 | return nil 427 | } 428 | f, err = os.Open(f.Name()) 429 | if err != nil { 430 | return err 431 | } 432 | nfi, err := f.Stat() 433 | if err != nil { 434 | return err 435 | } 436 | if nfi.ModTime().Equal(fi.ModTime()) { 437 | return nil 438 | } 439 | err = client.WriteStream(p, f, 0644) 440 | if err != nil { 441 | return err 442 | } 443 | case "exit": 444 | os.Exit(0) 445 | default: 446 | return errors.New("unknown command") 447 | } 448 | return nil 449 | } 450 | 451 | var localCommands = []string{"lpwd", "lmkdir", "lrm", "lrmdir"} 452 | var remoteCommands = []string{"cd", "pwd", "mkdir", "rm", "rmdir", "cat", "edit", "vim", "get", "cp", "mv"} 453 | var allCommands = []string{} 454 | 455 | func init() { 456 | allCommands = append(allCommands, localCommands...) 457 | allCommands = append(allCommands, remoteCommands...) 458 | } 459 | 460 | func isLocalCompletion(cmd string, narg int) (bool, bool) { 461 | if cmd == "put" { 462 | if narg == 2 { 463 | return false, false 464 | } else { 465 | return true, false 466 | } 467 | } 468 | for _, n := range []string{"put"} { 469 | if cmd == n { 470 | return true, false 471 | } 472 | } 473 | for _, n := range []string{"cat", "edit"} { 474 | if cmd == n { 475 | return false, false 476 | } 477 | } 478 | for _, n := range []string{"lcd", "lmkdir", "lrmdir"} { 479 | if cmd == n { 480 | return true, true 481 | } 482 | } 483 | for _, n := range []string{"cd", "mkdir", "rmdir"} { 484 | if cmd == n { 485 | return false, true 486 | } 487 | } 488 | for _, n := range localCommands { 489 | if cmd == n { 490 | return true, true 491 | } 492 | } 493 | return false, false 494 | } 495 | 496 | func complete(client *gowebdav.Client, cwd *string, l string) (c []string) { 497 | args, err := shellwords.Parse(string(l)) 498 | if err != nil || len(args) == 0 { 499 | return allCommands 500 | } 501 | if len(args) == 1 && !strings.HasSuffix(l, " ") { 502 | for _, cc := range allCommands { 503 | if strings.HasPrefix(cc, l) { 504 | c = append(c, cc) 505 | } 506 | } 507 | return 508 | } 509 | ncomplete := len(args) 510 | if len(args) > 1 && !strings.HasSuffix(l, " ") { 511 | ncomplete++ 512 | } 513 | var p string 514 | if local, listdir := isLocalCompletion(args[0], ncomplete); local { 515 | lwd, err := os.Getwd() 516 | if err != nil { 517 | return nil 518 | } 519 | if len(args) > 1 && !strings.HasSuffix(l, " ") { 520 | p = filepath.ToSlash(args[len(args)-1]) 521 | slashed := strings.HasSuffix(p, "/") 522 | if !filepath.IsAbs(p) { 523 | p = filepath.Join(lwd, p) 524 | if slashed && !strings.HasSuffix(p, "/") { 525 | p += "/" 526 | } 527 | } 528 | } else { 529 | p = lwd + "/" 530 | } 531 | dir, file := filepath.Split(p) 532 | f, err := os.Open(dir) 533 | if err != nil { 534 | return nil 535 | } 536 | defer f.Close() 537 | fis, err := f.Readdir(0) 538 | if err != nil { 539 | return nil 540 | } 541 | for _, fi := range fis { 542 | if listdir && !fi.IsDir() { 543 | continue 544 | } 545 | if len(file) == 0 { 546 | c = append(c, l+escape(fi.Name())) 547 | } else { 548 | if strings.HasPrefix(fi.Name(), file) { 549 | c = append(c, l+escape(fi.Name()[len(file):])) 550 | } 551 | } 552 | } 553 | } else { 554 | if len(args) > 1 && !strings.HasSuffix(l, " ") { 555 | p = args[len(args)-1] 556 | slashed := strings.HasSuffix(p, "/") 557 | if !path.IsAbs(p) { 558 | p = path.Join(*cwd, p) 559 | if slashed && !strings.HasSuffix(p, "/") { 560 | p += "/" 561 | } 562 | } 563 | } else { 564 | p = *cwd 565 | } 566 | dir, file := path.Split(p) 567 | fis, err := client.ReadDir(dir) 568 | if err != nil { 569 | return nil 570 | } 571 | for _, fi := range fis { 572 | if listdir && !fi.IsDir() { 573 | continue 574 | } 575 | if len(file) == 0 { 576 | c = append(c, l+escape(fi.Name())) 577 | } else { 578 | if strings.HasPrefix(fi.Name(), file) { 579 | c = append(c, l+escape(fi.Name()[len(file):])) 580 | } 581 | } 582 | } 583 | } 584 | return 585 | } 586 | 587 | func main() { 588 | flag.Parse() 589 | if flag.NArg() == 0 { 590 | flag.Usage() 591 | return 592 | } 593 | user, password := "", "" 594 | if *cred != "" { 595 | token := strings.SplitN(*cred, ":", 2) 596 | if len(token) != 2 { 597 | flag.Usage() 598 | return 599 | } 600 | user, password = token[0], token[1] 601 | } 602 | 603 | line := liner.NewLiner() 604 | defer line.Close() 605 | 606 | line.SetCtrlCAborts(true) 607 | 608 | u, err := url.Parse(flag.Arg(0)) 609 | if err != nil { 610 | fatal(err) 611 | } 612 | if u.Host == "" && u.Path != "" { 613 | u.Host, u.Path = u.Path, "" 614 | } 615 | 616 | switch u.Scheme { 617 | case "webdav", "http": 618 | u.Scheme = "http" 619 | case "webdavs", "https": 620 | u.Scheme = "https" 621 | default: 622 | u.Scheme = "https" 623 | } 624 | 625 | client := gowebdav.NewClient(u.Scheme+"://"+u.Host, user, password) 626 | err = client.Connect() 627 | if err != nil { 628 | ep, ok := err.(*os.PathError) 629 | if ok { 630 | err = ep.Err 631 | } 632 | switch err.Error() { 633 | case "200": 634 | case "401": 635 | if *cred != "" { 636 | fatal(err) 637 | } 638 | user, err = line.Prompt("User: ") 639 | if err != nil { 640 | fatalRequiredAuth(err) 641 | } 642 | password, err = line.PasswordPrompt("Password: ") 643 | if err != nil { 644 | fatalRequiredAuth(err) 645 | } 646 | client = gowebdav.NewClient(u.Scheme+"://"+u.Host, user, password) 647 | err = client.Connect() 648 | if err != nil { 649 | fatal(err) 650 | } 651 | default: 652 | fatal(err) 653 | } 654 | } 655 | 656 | cwd := u.Path 657 | if !strings.HasSuffix(cwd, "/") { 658 | cwd += "/" 659 | } 660 | if flag.NArg() == 1 { 661 | line := liner.NewLiner() 662 | defer line.Close() 663 | 664 | line.SetCompleter(func(l string) (c []string) { 665 | return complete(client, &cwd, l) 666 | }) 667 | 668 | for { 669 | prompt := "> " 670 | if *prompthere { 671 | prompt = cwd + "> " 672 | } 673 | l, err := line.Prompt(prompt) 674 | if err != nil { 675 | break 676 | } 677 | args, err := shellwords.Parse(l) 678 | if err != nil { 679 | fmt.Fprintln(color.Output, color.RedString("%v", err.Error())) 680 | continue 681 | } 682 | if len(args) == 0 { 683 | continue 684 | } 685 | line.AppendHistory(l) 686 | err = handle(client, &cwd, args) 687 | if err != nil { 688 | fmt.Fprintln(color.Output, color.RedString("%v", err.Error())) 689 | continue 690 | } 691 | } 692 | } else { 693 | err = handle(client, &cwd, flag.Args()[1:]) 694 | if err != nil { 695 | fmt.Fprintln(os.Stderr, err) 696 | } 697 | } 698 | } 699 | --------------------------------------------------------------------------------