github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/cmd/files.go (about) 1 package cmd 2 3 import ( 4 "encoding/json" 5 "errors" 6 "flag" 7 "fmt" 8 "io" 9 "mime" 10 "os" 11 "path" 12 "path/filepath" 13 "regexp" 14 "strconv" 15 "strings" 16 "time" 17 18 "github.com/cozy/cozy-stack/client" 19 "github.com/cozy/cozy-stack/pkg/consts" 20 humanize "github.com/dustin/go-humanize" 21 "github.com/spf13/cobra" 22 ) 23 24 var errFilesExec = errors.New("Bad usage of files exec") 25 26 const filesExecUsage = `Available commands: 27 28 mkdir <name> Creates a directory with specified name 29 ls [-l] [-a] [-h] <name> Prints the children of the specified directory 30 tree [-l] <name> Prints the tree structure of the specified directory 31 attrs <name> Prints the attributes of the specified file or directory 32 cat <name> Echo the file content in stdout 33 mv <from> <to> Rename a file or directory 34 rm [-f] [-r] <name> Move the file to trash, or delete it permanently with -f flag 35 restore <name> Restore a file or directory from trash 36 37 Don't forget to put quotes around the command! 38 ` 39 40 var flagImportFrom string 41 var flagImportTo string 42 var flagImportDryRun bool 43 var flagImportMatch string 44 var flagIncludeTrash bool 45 46 // filesCmdGroup represents the instances command 47 var filesCmdGroup = &cobra.Command{ 48 Use: "files <command>", 49 Short: "Interact with the cozy filesystem", 50 Long: ` 51 cozy-stack files allows to interact with the cozy filesystem. 52 53 It provides command to create, move copy or delete files and 54 directories inside your cozy instance, using the command line 55 interface. It also provide an import command to import from your 56 current filesystem into cozy. 57 `, 58 RunE: func(cmd *cobra.Command, args []string) error { 59 return cmd.Usage() 60 }, 61 } 62 63 var execFilesCmd = &cobra.Command{ 64 Use: "exec [--domain domain] <command>", 65 Short: "Execute the given command on the specified domain and leave", 66 Long: "Execute a command on the VFS of the specified domain.\n" + filesExecUsage, 67 RunE: func(cmd *cobra.Command, args []string) error { 68 if len(args) != 1 { 69 return cmd.Usage() 70 } 71 if flagDomain == "" { 72 errPrintfln("%s", errMissingDomain) 73 return cmd.Usage() 74 } 75 c := newClient(flagDomain, consts.Files) 76 command := args[0] 77 err := execCommand(c, command, os.Stdout) 78 if errors.Is(err, errFilesExec) { 79 return cmd.Usage() 80 } 81 return err 82 }, 83 } 84 85 var importFilesCmd = &cobra.Command{ 86 Use: "import [--domain domain] [--match pattern] --from <name> --to <name>", 87 Short: "Import the specified file or directory into cozy", 88 RunE: func(cmd *cobra.Command, args []string) error { 89 if flagDomain == "" { 90 errPrintfln("%s", errMissingDomain) 91 return cmd.Usage() 92 } 93 if flagImportFrom == "" || flagImportTo == "" { 94 return cmd.Usage() 95 } 96 97 var match *regexp.Regexp 98 if flagImportMatch != "" { 99 var err error 100 match, err = regexp.Compile(flagImportMatch) 101 if err != nil { 102 return err 103 } 104 } 105 106 c := newClient(flagDomain, consts.Files) 107 return importFiles(c, flagImportFrom, flagImportTo, match) 108 }, 109 } 110 111 var usageFilesCmd = &cobra.Command{ 112 Use: "usage [--domain domain] [--trash]", 113 Short: "Show the usage and quota for the files of this instance", 114 RunE: func(cmd *cobra.Command, args []string) error { 115 if flagDomain == "" { 116 errPrintfln("%s", errMissingDomain) 117 return cmd.Usage() 118 } 119 ac := newAdminClient() 120 info, err := ac.DiskUsage(flagDomain, flagIncludeTrash) 121 if err != nil { 122 return err 123 } 124 fmt.Fprintf(os.Stdout, "Usage: %v\n", info["used"]) 125 126 if files, ok := info["files"]; ok { 127 fmt.Fprintf(os.Stdout, " Including latest version of files: %v\n", files) 128 } 129 if versions, ok := info["versions"]; ok { 130 fmt.Fprintf(os.Stdout, " Including older versions of files: %v\n", versions) 131 } 132 133 if flagIncludeTrash { 134 if trashed, ok := info["trashed"]; ok { 135 fmt.Fprintf(os.Stdout, " Including trashed files: %v\n", trashed) 136 } 137 } 138 139 if quota, ok := info["quota"]; ok { 140 fmt.Fprintf(os.Stdout, "Quota: %v\n", quota) 141 } 142 if count, ok := info["doc_count"]; ok { 143 fmt.Fprintf(os.Stdout, "Documents count: %v\n", count) 144 } 145 if count, ok := info["versions_count"]; ok { 146 fmt.Fprintf(os.Stdout, "Versions Documents count: %v\n", count) 147 } 148 return nil 149 }, 150 } 151 152 func execCommand(c *client.Client, command string, w io.Writer) error { 153 args := splitArgs(command) 154 if len(args) == 0 { 155 return errFilesExec 156 } 157 158 cmdname := args[0] 159 160 fset := flag.NewFlagSet("", flag.ContinueOnError) 161 162 var flagMkdirP bool 163 var flagLsVerbose bool 164 var flagLsHuman bool 165 var flagLsAll bool 166 var flagRmForce bool 167 var flagRmRecur bool 168 169 switch cmdname { 170 case "mkdir": 171 fset.BoolVar(&flagMkdirP, "p", false, "Create intermediary directories") 172 case "ls": 173 fset.BoolVar(&flagLsVerbose, "l", false, "List in with additional attributes") 174 fset.BoolVar(&flagLsHuman, "h", false, "Print size in human readable format") 175 fset.BoolVar(&flagLsAll, "a", false, "Print hidden directories") 176 case "tree": 177 fset.BoolVar(&flagLsVerbose, "l", false, "List in with additional attributes") 178 case "rm": 179 fset.BoolVar(&flagRmForce, "f", false, "Delete file or directory permanently") 180 fset.BoolVar(&flagRmRecur, "r", false, "Delete directory and all its contents") 181 } 182 183 if err := fset.Parse(args[1:]); err != nil { 184 return err 185 } 186 187 args = fset.Args() 188 if len(args) == 0 { 189 return errFilesExec 190 } 191 192 switch cmdname { 193 case "mkdir": 194 return mkdirCmd(c, args[0], flagMkdirP) 195 case "ls": 196 return lsCmd(c, args[0], w, flagLsVerbose, flagLsHuman, flagLsAll) 197 case "tree": 198 return treeCmd(c, args[0], w, flagLsVerbose) 199 case "attrs": 200 return attrsCmd(c, args[0], w) 201 case "cat": 202 return catCmd(c, args[0], w) 203 case "mv": 204 if len(args) < 2 { 205 return errFilesExec 206 } 207 return mvCmd(c, args[0], args[1]) 208 case "rm": 209 return rmCmd(c, args[0], flagRmForce, flagRmRecur) 210 case "restore": 211 return restoreCmd(c, args[0]) 212 } 213 214 return errFilesExec 215 } 216 217 func mkdirCmd(c *client.Client, name string, mkdirP bool) error { 218 var err error 219 if mkdirP { 220 _, err = c.Mkdirall(name) 221 } else { 222 _, err = c.Mkdir(name) 223 } 224 return err 225 } 226 227 func lsCmd(c *client.Client, root string, w io.Writer, verbose, human, all bool) error { 228 type filePrint struct { 229 id string 230 typ string 231 name string 232 size string 233 mdate string 234 exec string 235 } 236 237 now := time.Now() 238 239 var prints []*filePrint 240 var maxnamelen int 241 var maxsizelen int 242 243 root = path.Clean(root) 244 245 err := c.WalkByPath(root, func(n string, doc *client.DirOrFile, err error) error { 246 if err != nil { 247 return err 248 } 249 if n == root && doc.Attrs.Type == consts.DirType { 250 return nil 251 } 252 253 attrs := doc.Attrs 254 var typ, size, mdate, exec string 255 name := attrs.Name 256 id := doc.ID 257 258 if now.Year() == attrs.UpdatedAt.Year() { 259 mdate = attrs.UpdatedAt.Format("Jan 02 15:04") 260 } else { 261 mdate = attrs.UpdatedAt.Format("Jan 02 2015") 262 } 263 264 if attrs.Type == consts.DirType { 265 typ = "d" 266 exec = "x" 267 } else { 268 typ = "-" 269 if attrs.Executable { 270 exec = "x" 271 } else { 272 exec = "-" 273 } 274 if human { 275 size = humanize.Bytes(uint64(attrs.Size)) 276 } else { 277 size = humanize.Comma(attrs.Size) 278 } 279 } 280 281 if len(name) > maxnamelen { 282 maxnamelen = len(name) 283 } 284 285 if len(size) > maxsizelen { 286 maxsizelen = len(size) 287 } 288 289 if all || len(name) == 0 || name[0] != '.' { 290 prints = append(prints, &filePrint{ 291 id: id, 292 typ: typ, 293 name: name, 294 size: size, 295 mdate: mdate, 296 exec: exec, 297 }) 298 } 299 if doc.Attrs.Type == client.DirType { 300 return filepath.SkipDir 301 } 302 return nil 303 }) 304 305 if err != nil { 306 return err 307 } 308 309 if !verbose { 310 for _, fp := range prints { 311 _, err = fmt.Fprintln(w, fp.name) 312 if err != nil { 313 return err 314 } 315 } 316 return nil 317 } 318 319 smaxsizelen := strconv.Itoa(maxsizelen) 320 smaxnamelen := strconv.Itoa(maxnamelen) 321 322 for _, fp := range prints { 323 _, err = fmt.Fprintf(w, "%s %s%s %"+smaxsizelen+"s %s %-"+smaxnamelen+"s\n", 324 fp.id, fp.typ, fp.exec, fp.size, fp.mdate, fp.name) 325 if err != nil { 326 return err 327 } 328 } 329 330 return nil 331 } 332 333 func treeCmd(c *client.Client, root string, w io.Writer, verbose bool) error { 334 root = path.Clean(root) 335 336 return c.WalkByPath(root, func(name string, doc *client.DirOrFile, err error) error { 337 if err != nil { 338 return err 339 } 340 if verbose { 341 fmt.Fprintf(os.Stdout, "%s ", doc.ID) 342 } 343 344 attrs := doc.Attrs 345 if name == root { 346 _, err = fmt.Fprintln(w, name) 347 return err 348 } 349 350 level := strings.Count(strings.TrimPrefix(name, root)[1:], "/") + 1 351 for i := 0; i < level; i++ { 352 if i == level-1 { 353 _, err = fmt.Fprintf(w, "└── ") 354 } else { 355 _, err = fmt.Fprintf(w, "| ") 356 } 357 if err != nil { 358 return err 359 } 360 } 361 _, err = fmt.Fprintln(w, attrs.Name) 362 return err 363 }) 364 } 365 366 func attrsCmd(c *client.Client, name string, w io.Writer) error { 367 doc, err := c.GetDirOrFileByPath(name) 368 if err != nil { 369 return err 370 } 371 enc := json.NewEncoder(w) 372 enc.SetIndent("", "\t") 373 return enc.Encode(doc) 374 } 375 376 func catCmd(c *client.Client, name string, w io.Writer) error { 377 r, err := c.DownloadByPath(name) 378 if err != nil { 379 return err 380 } 381 382 defer r.Close() 383 _, err = io.Copy(w, r) 384 385 return err 386 } 387 388 func mvCmd(c *client.Client, from, to string) error { 389 return c.Move(from, to) 390 } 391 392 func rmCmd(c *client.Client, name string, force, recur bool) error { 393 if force { 394 return c.PermanentDeleteByPath(name) 395 } 396 return c.TrashByPath(name) 397 } 398 399 func restoreCmd(c *client.Client, name string) error { 400 return c.RestoreByPath(name) 401 } 402 403 type importer struct { 404 c *client.Client 405 paths map[string]string 406 } 407 408 func (i *importer) mkdir(name string) (string, error) { 409 doc, err := i.c.Mkdirall(name) 410 if err != nil { 411 return "", err 412 } 413 i.paths[name] = doc.ID 414 return doc.ID, nil 415 } 416 417 func (i *importer) upload(localname, distname string) error { 418 var err error 419 420 dirname := path.Dir(distname) 421 dirID, ok := i.paths[dirname] 422 if !ok && dirname != string(os.PathSeparator) { 423 dirID, err = i.mkdir(dirname) 424 if err != nil { 425 return err 426 } 427 } 428 429 infos, err := os.Stat(localname) 430 if err != nil { 431 return err 432 } 433 434 r, err := os.Open(localname) 435 if err != nil { 436 return err 437 } 438 defer r.Close() 439 440 _, err = i.c.Upload(&client.Upload{ 441 Name: path.Base(distname), 442 DirID: dirID, 443 Contents: r, 444 ContentLength: infos.Size(), 445 ContentType: mime.TypeByExtension(localname), 446 }) 447 return err 448 } 449 450 func importFiles(c *client.Client, from, to string, match *regexp.Regexp) error { 451 from = path.Clean(from) 452 to = path.Clean(to) 453 454 i := &importer{ 455 c: c, 456 paths: make(map[string]string), 457 } 458 459 fromInfos, err := os.Stat(from) 460 if err != nil { 461 return err 462 } 463 if !fromInfos.IsDir() { 464 fmt.Fprintf(os.Stdout, "Importing file %s to cozy://%s\n", from, to) 465 return i.upload(from, to) 466 } 467 468 fmt.Fprintf(os.Stdout, "Importing from %s to cozy://%s\n", from, to) 469 470 return filepath.Walk(from, func(localname string, f os.FileInfo, err error) error { 471 if err != nil { 472 return err 473 } 474 475 if match != nil && !match.MatchString(localname) { 476 return nil 477 } 478 479 if localname == from { 480 if f.IsDir() { 481 return nil 482 } 483 return fmt.Errorf("Not a directory: %s", localname) 484 } 485 486 distname := path.Join(to, strings.TrimPrefix(localname, from)) 487 if f.IsDir() { 488 fmt.Fprintf(os.Stdout, "create dir %s\n", distname) 489 if !flagImportDryRun { 490 if _, err = i.mkdir(distname); err != nil { 491 return err 492 } 493 } 494 } else { 495 fmt.Fprintf(os.Stdout, "copying file %s to %s\n", localname, distname) 496 if !flagImportDryRun { 497 return i.upload(localname, distname) 498 } 499 } 500 501 return nil 502 }) 503 } 504 505 func splitArgs(command string) []string { 506 args := regexp.MustCompile("'.+'|\".+\"|\\S+").FindAllString(command, -1) 507 for i, a := range args { 508 l := len(a) 509 switch { 510 case a[0] == '\'' && a[l-1] == '\'': 511 args[i] = strings.Trim(a, "'") 512 case a[0] == '"' && a[l-1] == '"': 513 args[i] = strings.Trim(a, "\"") 514 } 515 } 516 return args 517 } 518 519 func init() { 520 filesCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance") 521 522 importFilesCmd.Flags().StringVar(&flagImportFrom, "from", "", "directory to import from in cozy") 523 _ = importFilesCmd.MarkFlagRequired("from") 524 importFilesCmd.Flags().StringVar(&flagImportTo, "to", "/", "directory to import to in cozy") 525 _ = importFilesCmd.MarkFlagRequired("to") 526 importFilesCmd.Flags().BoolVar(&flagImportDryRun, "dry-run", false, "do not actually import the files") 527 importFilesCmd.Flags().StringVar(&flagImportMatch, "match", "", "pattern that the imported files must match") 528 529 usageFilesCmd.Flags().BoolVar(&flagIncludeTrash, "trash", false, "Include trashed files total size") 530 531 filesCmdGroup.AddCommand(execFilesCmd) 532 filesCmdGroup.AddCommand(importFilesCmd) 533 filesCmdGroup.AddCommand(usageFilesCmd) 534 535 RootCmd.AddCommand(filesCmdGroup) 536 }