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  }