github.com/GGP1/kure@v0.8.4/commands/file/add/add.go (about) 1 package add 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/GGP1/kure/auth" 14 cmdutil "github.com/GGP1/kure/commands" 15 "github.com/GGP1/kure/db/file" 16 "github.com/GGP1/kure/pb" 17 "github.com/GGP1/kure/terminal" 18 19 "github.com/pkg/errors" 20 "github.com/spf13/cobra" 21 bolt "go.etcd.io/bbolt" 22 ) 23 24 const example = ` 25 * Add a new file 26 kure file add Sample -p path/to/file 27 28 * Add a note 29 kure file add Sample -n 30 31 * Add a folder and all its subfolders, limiting goroutine number to 40 32 kure file add Sample -p path/to/folder -s 40 33 34 * Add files from a folder, ignoring subfolders 35 kure file add Sample -p path/to/folder -i` 36 37 type addOptions struct { 38 path string 39 note bool 40 ignore bool 41 semaphore uint32 42 } 43 44 // NewCmd returns a new command. 45 func NewCmd(db *bolt.DB, r io.Reader) *cobra.Command { 46 opts := addOptions{} 47 cmd := &cobra.Command{ 48 Use: "add <name>", 49 Short: "Add files to the database", 50 Long: `Add files to the database. As they are stored in a database, the whole file is read into memory, please have this into account when adding new ones. 51 52 Path to a file must include its extension (in case it has one). 53 54 The user can specify a path to a folder as well, on this occasion, Kure will iterate over all the files in the folder and potential subfolders (if the -i flag is false) and store them into the database with the name "name/subfolders/filename". Empty folders will be skipped.`, 55 Aliases: []string{"new"}, 56 Example: example, 57 Args: cmdutil.MustNotExist(db, cmdutil.File), 58 PreRunE: auth.Login(db), 59 RunE: runAdd(db, r, &opts), 60 PostRun: func(cmd *cobra.Command, args []string) { 61 // Reset variables (session) 62 opts = addOptions{ 63 semaphore: 50, 64 } 65 }, 66 } 67 68 f := cmd.Flags() 69 f.BoolVarP(&opts.ignore, "ignore", "i", false, "ignore subfolders") 70 f.StringVarP(&opts.path, "path", "p", "", "path to the file/folder") 71 f.BoolVarP(&opts.note, "note", "n", false, "add a note") 72 f.Uint32VarP(&opts.semaphore, "semaphore", "s", 50, "maximum number of goroutines running concurrently") 73 74 return cmd 75 } 76 77 func runAdd(db *bolt.DB, r io.Reader, opts *addOptions) cmdutil.RunEFunc { 78 return func(cmd *cobra.Command, args []string) error { 79 name := strings.Join(args, " ") 80 name = cmdutil.NormalizeName(name) 81 82 if opts.note { 83 return addNote(db, r, name) 84 } 85 86 if opts.semaphore < 1 { 87 return errors.New("invalid semaphore quantity") 88 } 89 90 // Add the extension to differentiate between files and folders 91 if filepath.Ext(name) == "" && opts.path != "" { 92 name += filepath.Ext(opts.path) 93 } 94 95 dir, err := os.ReadDir(opts.path) 96 if err != nil { 97 // If it's not a directory, attempt storing a file 98 return storeFile(db, opts.path, name) 99 } 100 101 if len(dir) == 0 { 102 return errors.Errorf("%q directory is empty", filepath.Base(opts.path)) 103 } 104 105 // With filepath.Walk we can't associate each file 106 // with its folder, also, this may perform poorly on large directories. 107 // Allocations are reduced and performance improved. 108 var wg sync.WaitGroup 109 sem := make(chan struct{}, opts.semaphore) 110 wg.Add(len(dir)) 111 walkDir(db, dir, opts.path, name, opts.ignore, &wg, sem) 112 wg.Wait() 113 return nil 114 } 115 } 116 117 // walkDir iterates over the items of a folder and calls checkFile. 118 func walkDir(db *bolt.DB, dir []os.DirEntry, path, name string, ignore bool, wg *sync.WaitGroup, sem chan struct{}) { 119 for _, f := range dir { 120 // If it's not a directory or a regular file, skip 121 if !f.IsDir() && !f.Type().IsRegular() { 122 wg.Done() 123 continue 124 } 125 126 go checkFile(db, f, path, name, ignore, wg, sem) 127 } 128 } 129 130 // checkFile checks if the item is a file or a folder. 131 // 132 // If the item is a file, it stores it. 133 // 134 // If it's a folder it repeats the process until there are no left files to store. 135 // 136 // Errors are not returned but logged. 137 func checkFile(db *bolt.DB, file os.DirEntry, path, name string, ignore bool, wg *sync.WaitGroup, sem chan struct{}) { 138 defer func() { 139 wg.Done() 140 <-sem 141 }() 142 143 sem <- struct{}{} 144 145 // Join name (which is the folder name) with the file name (that could be a folder as well) 146 name = fmt.Sprintf("%s/%s", name, file.Name()) 147 // Prefer filepath.Join when working with paths only 148 path = filepath.Join(path, file.Name()) 149 150 if !file.IsDir() { 151 if err := storeFile(db, path, name); err != nil { 152 fmt.Fprintln(os.Stderr, "error:", err) 153 } 154 return 155 } 156 157 // Ignore subfolders 158 if ignore { 159 return 160 } 161 162 subdir, err := os.ReadDir(path) 163 if err != nil { 164 fmt.Fprintf(os.Stderr, "error: reading directory: %v\n", err) 165 return 166 } 167 168 if len(subdir) != 0 { 169 wg.Add(len(subdir)) 170 go walkDir(db, subdir, path, name, ignore, wg, sem) 171 } 172 } 173 174 // storeFile reads and saves a file into the database. 175 func storeFile(db *bolt.DB, path, filename string) error { 176 content, err := os.ReadFile(path) 177 if err != nil { 178 return errors.Wrap(err, "reading file") 179 } 180 181 f := &pb.File{ 182 Name: strings.ToLower(filename), 183 Content: content, 184 Size: int64(len(content)), 185 CreatedAt: time.Now().Unix(), 186 UpdatedAt: time.Time{}.Unix(), 187 } 188 189 // There is no better way to report as Batch combines 190 // all the transactions into a single one 191 abs, _ := filepath.Abs(path) 192 193 fmt.Println("Add:", abs) 194 return file.Create(db, f) 195 } 196 197 // addNote takes input from the user and creates a file inside the "notes" folder 198 // and with the .txt extension. 199 func addNote(db *bolt.DB, r io.Reader, name string) error { 200 name = "notes/" + name 201 if filepath.Ext(name) == "" { 202 name += ".txt" 203 } 204 205 if err := cmdutil.Exists(db, name, cmdutil.File); err != nil { 206 return err 207 } 208 209 text := terminal.Scanlns(bufio.NewReader(r), "Text") 210 211 f := &pb.File{ 212 Name: name, 213 Content: []byte(text), 214 Size: int64(len(text)), 215 CreatedAt: time.Now().Unix(), 216 UpdatedAt: time.Time{}.Unix(), 217 } 218 219 fmt.Println("Add:", name) 220 return file.Create(db, f) 221 }