package cmd import ( "encoding/json" "errors" "flag" "fmt" "io" "mime" "os" "path" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/cozy/cozy-stack/client" "github.com/cozy/cozy-stack/pkg/consts" humanize "github.com/dustin/go-humanize" "github.com/spf13/cobra" ) var errFilesExec = errors.New("Bad usage of files exec") const filesExecUsage = `Available commands: mkdir Creates a directory with specified name ls [-l] [-a] [-h] Prints the children of the specified directory tree [-l] Prints the tree structure of the specified directory attrs Prints the attributes of the specified file or directory cat Echo the file content in stdout mv Rename a file or directory rm [-f] [-r] Move the file to trash, or delete it permanently with -f flag restore Restore a file or directory from trash Don't forget to put quotes around the command! ` var flagImportFrom string var flagImportTo string var flagImportDryRun bool var flagImportMatch string var flagIncludeTrash bool // filesCmdGroup represents the instances command var filesCmdGroup = &cobra.Command{ Use: "files ", Short: "Interact with the cozy filesystem", Long: ` cozy-stack files allows to interact with the cozy filesystem. It provides command to create, move copy or delete files and directories inside your cozy instance, using the command line interface. It also provide an import command to import from your current filesystem into cozy. `, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Usage() }, } var execFilesCmd = &cobra.Command{ Use: "exec [--domain domain] ", Short: "Execute the given command on the specified domain and leave", Long: "Execute a command on the VFS of the specified domain.\n" + filesExecUsage, RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return cmd.Usage() } if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } c := newFilesClient(flagDomain, "") command := args[0] err := execCommand(c, command, os.Stdout) if errors.Is(err, errFilesExec) { return cmd.Usage() } return err }, } var importFilesCmd = &cobra.Command{ Use: "import [--domain domain] [--match pattern] --from --to ", Short: "Import the specified file or directory into cozy", RunE: func(cmd *cobra.Command, args []string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } if flagImportFrom == "" || flagImportTo == "" { return cmd.Usage() } var match *regexp.Regexp if flagImportMatch != "" { var err error match, err = regexp.Compile(flagImportMatch) if err != nil { return err } } c := newFilesClient(flagDomain, "") return importFiles(c, flagImportFrom, flagImportTo, match) }, } var usageFilesCmd = &cobra.Command{ Use: "usage [--domain domain] [--trash]", Short: "Show the usage and quota for the files of this instance", RunE: func(cmd *cobra.Command, args []string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } ac := newAdminClient() info, err := ac.DiskUsage(flagDomain, flagIncludeTrash) if err != nil { return err } fmt.Fprintf(os.Stdout, "Usage: %v\n", info["used"]) if files, ok := info["files"]; ok { fmt.Fprintf(os.Stdout, " Including latest version of files: %v\n", files) } if versions, ok := info["versions"]; ok { fmt.Fprintf(os.Stdout, " Including older versions of files: %v\n", versions) } if flagIncludeTrash { if trashed, ok := info["trashed"]; ok { fmt.Fprintf(os.Stdout, " Including trashed files: %v\n", trashed) } } if quota, ok := info["quota"]; ok { fmt.Fprintf(os.Stdout, "Quota: %v\n", quota) } if count, ok := info["doc_count"]; ok { fmt.Fprintf(os.Stdout, "Documents count: %v\n", count) } if count, ok := info["versions_count"]; ok { fmt.Fprintf(os.Stdout, "Versions Documents count: %v\n", count) } return nil }, } func execCommand(c *client.FilesClient, command string, w io.Writer) error { args := splitArgs(command) if len(args) == 0 { return errFilesExec } cmdname := args[0] fset := flag.NewFlagSet("", flag.ContinueOnError) var flagMkdirP bool var flagLsVerbose bool var flagLsHuman bool var flagLsAll bool var flagRmForce bool var flagRmRecur bool switch cmdname { case "mkdir": fset.BoolVar(&flagMkdirP, "p", false, "Create intermediary directories") case "ls": fset.BoolVar(&flagLsVerbose, "l", false, "List in with additional attributes") fset.BoolVar(&flagLsHuman, "h", false, "Print size in human readable format") fset.BoolVar(&flagLsAll, "a", false, "Print hidden directories") case "tree": fset.BoolVar(&flagLsVerbose, "l", false, "List in with additional attributes") case "rm": fset.BoolVar(&flagRmForce, "f", false, "Delete file or directory permanently") fset.BoolVar(&flagRmRecur, "r", false, "Delete directory and all its contents") } if err := fset.Parse(args[1:]); err != nil { return err } args = fset.Args() if len(args) == 0 { return errFilesExec } switch cmdname { case "mkdir": return mkdirCmd(c, args[0], flagMkdirP) case "ls": return lsCmd(c, args[0], w, flagLsVerbose, flagLsHuman, flagLsAll) case "tree": return treeCmd(c, args[0], w, flagLsVerbose) case "attrs": return attrsCmd(c, args[0], w) case "cat": return catCmd(c, args[0], w) case "mv": if len(args) < 2 { return errFilesExec } return mvCmd(c, args[0], args[1]) case "rm": return rmCmd(c, args[0], flagRmForce, flagRmRecur) case "restore": return restoreCmd(c, args[0]) } return errFilesExec } func mkdirCmd(c *client.FilesClient, name string, mkdirP bool) error { var err error if mkdirP { _, err = c.Mkdirall(name) } else { _, err = c.Mkdir(name) } return err } func lsCmd(c *client.FilesClient, root string, w io.Writer, verbose, human, all bool) error { type filePrint struct { id string typ string name string size string mdate string exec string } now := time.Now() var prints []*filePrint var maxnamelen int var maxsizelen int root = path.Clean(root) err := c.WalkByPath(root, func(n string, doc *client.DirOrFile, err error) error { if err != nil { return err } if n == root && doc.Attrs.Type == consts.DirType { return nil } attrs := doc.Attrs var typ, size, mdate, exec string name := attrs.Name id := doc.ID if now.Year() == attrs.UpdatedAt.Year() { mdate = attrs.UpdatedAt.Format("Jan 02 15:04") } else { mdate = attrs.UpdatedAt.Format("Jan 02 2015") } if attrs.Type == consts.DirType { typ = "d" exec = "x" } else { typ = "-" if attrs.Executable { exec = "x" } else { exec = "-" } if human { size = humanize.Bytes(uint64(attrs.Size)) } else { size = humanize.Comma(attrs.Size) } } if len(name) > maxnamelen { maxnamelen = len(name) } if len(size) > maxsizelen { maxsizelen = len(size) } if all || len(name) == 0 || name[0] != '.' { prints = append(prints, &filePrint{ id: id, typ: typ, name: name, size: size, mdate: mdate, exec: exec, }) } if doc.Attrs.Type == client.DirType { return filepath.SkipDir } return nil }) if err != nil { return err } if !verbose { for _, fp := range prints { _, err = fmt.Fprintln(w, fp.name) if err != nil { return err } } return nil } smaxsizelen := strconv.Itoa(maxsizelen) smaxnamelen := strconv.Itoa(maxnamelen) for _, fp := range prints { _, err = fmt.Fprintf(w, "%s %s%s %"+smaxsizelen+"s %s %-"+smaxnamelen+"s\n", fp.id, fp.typ, fp.exec, fp.size, fp.mdate, fp.name) if err != nil { return err } } return nil } func treeCmd(c *client.FilesClient, root string, w io.Writer, verbose bool) error { root = path.Clean(root) return c.WalkByPath(root, func(name string, doc *client.DirOrFile, err error) error { if err != nil { return err } if verbose { fmt.Fprintf(os.Stdout, "%s ", doc.ID) } attrs := doc.Attrs if name == root { _, err = fmt.Fprintln(w, name) return err } level := strings.Count(strings.TrimPrefix(name, root)[1:], "/") + 1 for i := 0; i < level; i++ { if i == level-1 { _, err = fmt.Fprintf(w, "└── ") } else { _, err = fmt.Fprintf(w, "| ") } if err != nil { return err } } _, err = fmt.Fprintln(w, attrs.Name) return err }) } func attrsCmd(c *client.FilesClient, name string, w io.Writer) error { doc, err := c.GetDirOrFileByPath(name) if err != nil { return err } enc := json.NewEncoder(w) enc.SetIndent("", "\t") return enc.Encode(doc) } func catCmd(c *client.FilesClient, name string, w io.Writer) error { r, err := c.DownloadByPath(name) if err != nil { return err } defer r.Close() _, err = io.Copy(w, r) return err } func mvCmd(c *client.FilesClient, from, to string) error { return c.Move(from, to) } func rmCmd(c *client.FilesClient, name string, force, recur bool) error { if force { return c.PermanentDeleteByPath(name) } return c.TrashByPath(name) } func restoreCmd(c *client.FilesClient, name string) error { return c.RestoreByPath(name) } type importer struct { c *client.FilesClient paths map[string]string } func (i *importer) mkdir(name string) (string, error) { doc, err := i.c.Mkdirall(name) if err != nil { return "", err } i.paths[name] = doc.ID return doc.ID, nil } func (i *importer) upload(localname, distname string) error { var err error dirname := path.Dir(distname) dirID, ok := i.paths[dirname] if !ok && dirname != string(os.PathSeparator) { dirID, err = i.mkdir(dirname) if err != nil { return err } } infos, err := os.Stat(localname) if err != nil { return err } r, err := os.Open(localname) if err != nil { return err } defer r.Close() _, err = i.c.Upload(&client.Upload{ Name: path.Base(distname), DirID: dirID, Contents: r, ContentLength: infos.Size(), ContentType: mime.TypeByExtension(localname), }) return err } func importFiles(c *client.FilesClient, from, to string, match *regexp.Regexp) error { from = path.Clean(from) to = path.Clean(to) i := &importer{ c: c, paths: make(map[string]string), } fromInfos, err := os.Stat(from) if err != nil { return err } if !fromInfos.IsDir() { fmt.Fprintf(os.Stdout, "Importing file %s to cozy://%s\n", from, to) return i.upload(from, to) } fmt.Fprintf(os.Stdout, "Importing from %s to cozy://%s\n", from, to) return filepath.Walk(from, func(localname string, f os.FileInfo, err error) error { if err != nil { return err } if match != nil && !match.MatchString(localname) { return nil } if localname == from { if f.IsDir() { return nil } return fmt.Errorf("Not a directory: %s", localname) } distname := path.Join(to, strings.TrimPrefix(localname, from)) if f.IsDir() { fmt.Fprintf(os.Stdout, "create dir %s\n", distname) if !flagImportDryRun { if _, err = i.mkdir(distname); err != nil { return err } } } else { fmt.Fprintf(os.Stdout, "copying file %s to %s\n", localname, distname) if !flagImportDryRun { return i.upload(localname, distname) } } return nil }) } func splitArgs(command string) []string { args := regexp.MustCompile("'.+'|\".+\"|\\S+").FindAllString(command, -1) for i, a := range args { l := len(a) switch { case a[0] == '\'' && a[l-1] == '\'': args[i] = strings.Trim(a, "'") case a[0] == '"' && a[l-1] == '"': args[i] = strings.Trim(a, "\"") } } return args } func init() { filesCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance") importFilesCmd.Flags().StringVar(&flagImportFrom, "from", "", "directory to import from in cozy") _ = importFilesCmd.MarkFlagRequired("from") importFilesCmd.Flags().StringVar(&flagImportTo, "to", "/", "directory to import to in cozy") _ = importFilesCmd.MarkFlagRequired("to") importFilesCmd.Flags().BoolVar(&flagImportDryRun, "dry-run", false, "do not actually import the files") importFilesCmd.Flags().StringVar(&flagImportMatch, "match", "", "pattern that the imported files must match") usageFilesCmd.Flags().BoolVar(&flagIncludeTrash, "trash", false, "Include trashed files total size") filesCmdGroup.AddCommand(execFilesCmd) filesCmdGroup.AddCommand(importFilesCmd) filesCmdGroup.AddCommand(usageFilesCmd) RootCmd.AddCommand(filesCmdGroup) }