diff --git a/cmd/find-main.go b/cmd/find-main.go new file mode 100644 index 0000000000..eb4d5d13ce --- /dev/null +++ b/cmd/find-main.go @@ -0,0 +1,191 @@ +/* + * Minio Client (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package cmd stores all of the mc utilities +package cmd + +import ( + "strings" + + "github.com/fatih/color" + "github.com/minio/cli" + "github.com/minio/mc/pkg/console" +) + +// find specific flags +var ( + findFlags = []cli.Flag{ + cli.StringFlag{ + Name: "name", + Usage: "Find object names matching wildcard pattern", + }, + cli.StringFlag{ + Name: "path", + Usage: "Match directory names matching wildcard pattern", + }, + cli.StringFlag{ + Name: "regex", + Usage: "Match directory and object name with PCRE regex pattern", + }, + cli.StringFlag{ + Name: "print", + Usage: "Print in custom format to STDOUT (see FORMAT)", + }, + cli.StringFlag{ + Name: "exec", + Usage: "Spawn an external process for each matching object (see FORMAT)", + }, + cli.StringFlag{ + Name: "ignore", + Usage: "Exclude objects matching the wildcard pattern", + }, + cli.StringFlag{ + Name: "newer", + Usage: "Match all objects newer than specified time in units (see UNITS)", + }, + cli.StringFlag{ + Name: "older", + Usage: "Match all objects older than specified time in units (see UNITS)", + }, + cli.StringFlag{ + Name: "larger", + Usage: "Match all objects larger than specified size in units (see UNITS)", + }, + cli.StringFlag{ + Name: "smaller", + Usage: "Match all objects smaller than specified size in units (see UNITS)", + }, + cli.StringFlag{ + Name: "maxdepth", + Usage: "Limit directory navigation to specified depth", + }, + cli.BoolFlag{ + Name: "watch", + Usage: "Monitor specified location for newly created objects", + }, + cli.BoolFlag{ + Name: "or", + Usage: "Changes the matching criteria from an \"and\" to an \"or\"", + }, + } +) + +var findCmd = cli.Command{ + Name: "find", + Usage: "Finds files which match the given set of parameters.", + Action: mainFind, + Flags: append(findFlags, globalFlags...), + CustomHelpTemplate: `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{.HelpName}} PATH FLAG EXPRESSION [FLAG] + +FLAGS: + {{range .VisibleFlags}}{{.}} + {{end}} +EXAMPLES: + 1. Find all files named foo from all buckets. + $ {{.HelpName}} s3 --name "file" + + 2. Find all text files from mybucket. + $ {{.HelpName}} s3/mybucket --name "*.txt" + + 3. Print only the object names without the directory component under this bucket. + $ {{.HelpName}} s3/bucket --name "*" -print {base} + + 4. Copy all jpg files from AWS S3 photos bucket to minio play test bucket. + $ {{.HelpName}} s3/photos --name "*.jpg" --exec "mc cp {} play/test" + + 5. Find all jpg images from any folder prefixed with album. + $ {{.HelpName}} s3/photos --name "*.jpg" --path "*/album*/*" + + 6. Find all jpgs, pngs, and gifs using regex + $ {{.HelpName}} s3/photos --regex "(?i)\.(jpg|png|gif)$" + + 7. Mirror all photos from s3 bucket *coninuously* from the s3 bucket to minio play test bucket. + $ {{.HelpName}} s3/buck --name "*foo" --watch --exec "mc cp {} play/test" + + 8. Generate self expiring urls (7 days), for all objects between 64 MB, and 1 GB in size. + $ {{.HelpName}} s3 --larger 64MB --smaller 1GB --print {url} + + 9. Find all files under the s3 bucket which were created within a week. + $ {{.HelpName}} s3/bucket --newer 1w + + 10. Find all files which were created more than 6 months ago ignoring files ending in jpg. + $ {{.HelpName}} s3 --older 6m --ignore "*.jpg" + + 11. List all objects up to 3 levels subdirectory deep. + $ {{.HelpName}} s3/bucket --maxdepth 3 + +UNITS + + --smaller, --larger flags accept human-readable case-insensitive number suffixes such as "k", "m", "g" and "t" referring to the metric units KB, MB, GB and TB respectively. Adding an "i" to these prefixes, uses the IEC units, so that "gi" refers to "gibibyte" or "GiB". A "b" at the end is also accepted. Without suffixes the unit is bytes. + + --older, --newer flags accept the suffixes "d", "w", "m" and "y" to refer to units of days, weeks, months and years respectively. With the standard rate of conversion being 7 days being in 1 week, 30 days in 1 month, and 365 days in one year. + + +`, +} + +// checkFindSyntax - validate the passed arguments +func checkFindSyntax(ctx *cli.Context) { + args := ctx.Args() + + // help message on [mc][find] + if !args.Present() { + cli.ShowCommandHelpAndExit(ctx, "find", 1) + } + + if ctx.Bool("watch") && !strings.Contains(args[0], "/") { + console.Println("Users must specify a bucket name for watch") + console.Fatalln() + } + + // verify that there are no empty arguments + for _, arg := range args { + if strings.TrimSpace(arg) == "" { + fatalIf(errInvalidArgument().Trace(args...), "Unable to validate empty argument.") + } + } + +} + +// mainFind - handler for mc find commands +func mainFind(ctx *cli.Context) error { + // Additional command specific theme customization. + console.SetColor("Find", color.New(color.FgGreen, color.Bold)) + + var cErr error + + checkFindSyntax(ctx) + + args := ctx.Args() + + if !ctx.Args().Present() { + args = []string{"."} + } + + for _, targetURL := range args { + var clnt Client + clnt, err := newClient(targetURL) + fatalIf(err.Trace(targetURL), "Unable to initialize `"+targetURL+"`") + + DoFind(clnt, ctx) + + } + return cErr +} diff --git a/cmd/find.go b/cmd/find.go new file mode 100644 index 0000000000..e683b48170 --- /dev/null +++ b/cmd/find.go @@ -0,0 +1,490 @@ +/* + * Minio Client (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package cmd stores all of the mc utilities +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/dustin/go-humanize" + "github.com/minio/cli" + "github.com/minio/mc/pkg/console" + "github.com/minio/mc/pkg/probe" + + // golang does not support flat keys for path matching, find does + "github.com/minio/minio/pkg/wildcard" +) + +// findMSG holds JSON and string values for printing +type findMSG struct { + Path string `json:"path"` +} + +// String calls tells the console what to print and how to print it +func (f findMSG) String() string { + return console.Colorize("Find", f.Path) +} + +// JSON formats output to be JSON output +func (f findMSG) JSON() string { + f.Path = "path" + jsonMessageBytes, e := json.Marshal(f) + fatalIf(probe.NewError(e), "Unable to marshal into JSON.") + + return string(jsonMessageBytes) +} + +// nameMatch pattern matches off of the base of the filepath +func nameMatch(path, pattern string) (bool, error) { + base := filepath.Base(path) + + return filepath.Match(pattern, base) +} + +// pathMatch pattern matches off of of the entire filepath +func pathMatch(path, pattern string) bool { + return wildcard.Match(pattern, path) +} + +// regexMatch pattern matches off of the entire filepath using regex library +func regexMatch(path, pattern string) (bool, error) { + return regexp.MatchString(pattern, path) +} + +// doFindPrint prints the output in accordance with the supplied substitution arguments +func doFindPrint(path string, ctx *cli.Context, fileContent contentMessage) { + printString := SubArgsHelper(ctx.String("print"), path, fileContent) + printMsg(findMSG{ + Path: printString, + }) +} + +// doFindExec passes the users input along to the command line, also dealing with substitution arguments +func doFindExec(ctx *cli.Context, path string, fileContent contentMessage) { + commandString := SubArgsHelper(ctx.String("exec"), path, fileContent) + commandArgs := strings.Split(commandString, " ") + + cmd := exec.Command(commandArgs[0], commandArgs[1:]...) + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + console.Fatalln(err) + console.Fatalln() + } + console.Println(string(out.Bytes())) +} + +// doFindWatch watches the server side to see if a given action is preformed, if yes then a can provided by exec can be executed +func doFindWatch(path string, ctx *cli.Context) { + params := watchParams{ + recursive: true, + accountID: fmt.Sprintf("%d", time.Now().Unix()), + events: []string{"put"}, + } + + pathnameParts := strings.SplitAfter(path, "/") + alias := strings.TrimSuffix(pathnameParts[0], "/") + + _, e := getHostConfig(alias) // extract the hostname alias from the path name if present + noAlias := (e == nil) + + clnt, content, err := url2Stat(path) + fatalIf(err, "Unable to construct client") + + fileContent := parseContent(content) + if noAlias { + //pass hostname (alias) to to watchEvents so that it can remove from pathnames returned from watch + watchEvents(ctx, clnt, params, alias, fileContent) + return + } + watchEvents(ctx, clnt, params, "", fileContent) + return +} + +// doFindOlder checks to see if the given object was created before or after the given time at which an object was created +func doFindOlder(createTime time.Time, pattern string) bool { + i, err := TimeHelper(pattern) + now := time.Now() + fatalIf(probe.NewError(err), "Error parsing string passed to flag older") + + //find all time in which the time in which the object was just created is after the current time + t := time.Date(now.Year(), now.Month(), now.Day()-i, now.Hour(), now.Minute(), 0, 0, time.UTC) + return createTime.Before(t) +} + +// doFindNewer checks to see if the given object was created before the given threshold +func doFindNewer(createTime time.Time, pattern string) bool { + i, err := TimeHelper(pattern) + now := time.Now() + + fatalIf(probe.NewError(err), "Error parsing string passed to flag newer") + + t := time.Date(now.Year(), now.Month(), now.Day()-i, now.Hour(), now.Minute(), 0, 0, time.UTC) + return createTime.After(t) || createTime.Equal(t) +} + +// doFindLargerSize checks to see if the given object is larger than the given threshold +func doFindLargerSize(size int64, pattern string) bool { + i, err := humanize.ParseBytes(pattern) + fatalIf(probe.NewError(err), "Error parsing string passed to flag larger") + + return int64(i) < size +} + +// doFindSmallerSize checks to see if the given object is smaller than the given threshold +func doFindSmallerSize(size int64, pattern string) bool { + i, err := humanize.ParseBytes(pattern) + fatalIf(probe.NewError(err), "Error parsing string passed to flag smaller") + + return int64(i) > size +} + +// DoFind is used to handle most of the users input +func DoFind(clnt Client, ctx *cli.Context) { + pathnameParts := strings.SplitAfter(ctx.Args().Get(0), "/") + alias := strings.TrimSuffix(pathnameParts[0], "/") + _, err := getHostConfig(alias) + + // iterate over all content which is within the given directory + for content := range clnt.List(true, false, DirNone) { + fileContent := parseContent(content) + filePath := fileContent.Key + + // traversing in a object store not a file path + if err == nil { + filePath = path.Join(alias, filePath) + } + + if ctx.String("maxdepth") != "" { + i, e := strconv.Atoi(ctx.String("maxdepth")) + s := "" + + fatalIf(probe.NewError(e), "Error parsing string passed to flag maxdepth") + + // we are going to be parsing the path by x amounts + pathParts := strings.SplitAfter(filePath, "/") + + // handle invalid params + // ex. user specifies: + // maxdepth 2, but the given object only has a maxdepth of 1 + if (len(pathParts)-1) < i || i < 0 { + + // -1 is meant to handle each the array being 0 indexed, but the size not being 0 indexed + i = len(pathParts) - 1 + } + + // append portions of path into a string + for j := 0; j <= i; j++ { + s += pathParts[j] + } + + filePath = s + fileContent.Key = s + } + + // maxdepth can modify the filepath to end in a directory + // to be consistent with find we do not want to be listing directories + // so any parms which end in / will be ignored + if !strings.HasSuffix(filePath, "/") { + + orBool := ctx.Bool("or") + + match := fileContentMatch(fileContent, orBool, ctx) + + if match && ctx.String("print") != "" { + doFindPrint(filePath, ctx, fileContent) + } else if match { + printMsg(findMSG{ + Path: filePath, + }) + } + + if !ctx.Bool("watch") && match && ctx.String("exec") != "" { + doFindExec(ctx, filePath, fileContent) + } + } + + if ctx.Bool("watch") { + doFindWatch(ctx.Args().Get(0), ctx) + } + } + +} + +// SubArgsHelper formats the string to remove {} and replace each with the appropriate argument +func SubArgsHelper(args, path string, fileContent contentMessage) string { + + // replace all instances of {} + str := args + if strings.Contains(str, "{}") { + str = strings.Replace(str, "{}", path, -1) + } + + // replace all instances of {base} + if strings.Contains(str, "{base}") { + str = strings.Replace(str, "{base}", filepath.Base(path), -1) + } + + // replace all instances of {dir} + if strings.Contains(str, "{dir}") { + str = strings.Replace(str, "{dir}", filepath.Dir(path), -1) + } + + // replace all instances of {size} + if strings.Contains(str, "{size}") { + s := humanize.IBytes(uint64(fileContent.Size)) + str = strings.Replace(str, "{size}", s, -1) + } + + if strings.Contains(str, "{url}") { + s := GetPurl(path) + str = strings.Replace(str, "{url}", s, -1) + } + + if strings.Contains(str, "{time}") { + t := fileContent.Time.String() + str = strings.Replace(str, "{time}", t, -1) + } + + // replace all instances of {""} + if strings.Contains(str, "{\""+"\"}") { + str = strings.Replace(str, "{\""+"\"}", strconv.Quote(path), -1) + } + + // replace all instances of {"base"} + if strings.Contains(str, "{\""+"base"+"\"}") { + str = strings.Replace(str, "{\""+"base"+"\"}", strconv.Quote(filepath.Base(path)), -1) + } + + // replace all instances of {"dir"} + if strings.Contains(str, "{\""+"dir"+"\"}") { + str = strings.Replace(str, "{\""+"dir"+"\"}", strconv.Quote(filepath.Dir(path)), -1) + } + + if strings.Contains(str, "{\""+"url"+"\"}") { + s := GetPurl(path) + str = strings.Replace(str, "{\""+"url"+"\"}", strconv.Quote(s), -1) + } + + if strings.Contains(str, "{\""+"size"+"\"}") { + s := humanize.IBytes(uint64(fileContent.Size)) + str = strings.Replace(str, "{\""+"size"+"\"}", strconv.Quote(s), -1) + } + + if strings.Contains(str, "{\""+"time"+"\"}") { + str = strings.Replace(str, "{\""+"time"+"\"}", strconv.Quote(fileContent.Time.String()), -1) + } + + return str +} + +// watchEvents used in conjunction with doFindWatch method to detect and preform desired action when an object is created +func watchEvents(ctx *cli.Context, clnt Client, params watchParams, alias string, fileContent contentMessage) { + watchObj, err := clnt.Watch(params) + fatalIf(err, "Cannot watch with given params") + + // get client url and remove + cliTemp, _ := getHostConfig(alias) + cliTempURL := cliTemp.URL + + // enables users to kill using the control + c + trapCh := signalTrap(os.Interrupt, syscall.SIGTERM) + + wg := sync.WaitGroup{} + wg.Add(1) + + // opens a channel of all content created on the server, any errors detected, + // and scanning for the user input (Control + C) + go func() { + defer wg.Done() + + // loop until user kills the channel input + for { + select { + case <-trapCh: + console.Println() + close(watchObj.doneChan) + return + case event, ok := <-watchObj.Events(): + if !ok { + return + } + + time, _ := time.Parse(time.RFC822, event.Time) + msg := contentMessage{ + Key: alias + strings.TrimPrefix(event.Path, cliTempURL), + Time: time, + Size: event.Size, + } + + msg.Key = alias + msg.Key + + // check to see if the newly create object matches the given params + match := fileContentMatch(msg, ctx.Bool("or"), ctx) + + if match && ctx.String("exec") != "" { + doFindExec(ctx, msg.Key, fileContent) + } + + if match && ctx.String("print") != "" { + doFindExec(ctx, msg.Key, fileContent) + } else if match { + printMsg(findMSG{ + Path: msg.Key, + }) + } + + case err, ok := <-watchObj.Errors(): + if !ok { + return + } + errorIf(err, "Unable to watch for events.") + return + } + } + }() + + wg.Wait() +} + +// fileContentMatch is used to take the params passed to find, in addition to the current +// file and call the appropriate "pattern matching methods" +func fileContentMatch(fileContent contentMessage, orOp bool, ctx *cli.Context) bool { + match := true + + if (match && !orOp) && ctx.String("ignore") != "" { + match = !pathMatch(fileContent.Key, ctx.String("ignore")) + } + + // verify that the newly added object matches all of the other specified params + if (match || orOp) && ctx.String("name") != "" { + tmp, err := nameMatch(fileContent.Key, ctx.String("name")) + match = tmp + + fatalIf(probe.NewError(err), "Name could not be matched") + } + + if (match || orOp) && ctx.String("path") != "" { + match = pathMatch(fileContent.Key, ctx.String("path")) + } + + if (match || orOp) && ctx.String("regex") != "" { + temp, e := regexMatch(fileContent.Key, ctx.String("regex")) + match = temp + + fatalIf(probe.NewError(e), "Regex could not be matched") + } + + if (match || orOp) && ctx.String("older") != "" { + match = doFindOlder(fileContent.Time, ctx.String("older")) + } + + if (match || orOp) && ctx.String("newer") != "" { + match = doFindNewer(fileContent.Time, ctx.String("newer")) + } + + if (match || orOp) && ctx.String("larger") != "" { + match = doFindLargerSize(fileContent.Size, ctx.String("larger")) + } + + if (match || orOp) && ctx.String("smaller") != "" { + match = doFindSmallerSize(fileContent.Size, ctx.String("smaller")) + } + + return match +} + +// TimeHelper is used in conjunction with the Newer and Older flags to convert the input +// into an interperable integer (in days) +func TimeHelper(pattern string) (int, error) { + var i int + var t string + var err error + conversion := map[string]int{ + "d": 1, + "w": 7, + "m": 30, + "y": 365, + } + i, err = strconv.Atoi(pattern) + if err != nil { + t = pattern[len(pattern)-2:] + i, err = strconv.Atoi(pattern[:len(pattern)-2]) + + if err != nil { + return 0, err + + } + } + + return i * conversion[strings.ToLower(t)], nil +} + +// GetPurl is used in conjunction with the {url} substitution argument to return presigned URLs +func GetPurl(path string) string { + targetAlias, targetURLFull, _, err := expandAlias(path) + fatalIf(err, "Error with expand alias") + clnt, err := newClientFromAlias(targetAlias, targetURLFull) + fatalIf(err, "Error with newClientFromAlias") + + isIncomplete := false + + objectsCh := make(chan *clientContent) + + content, err := clnt.Stat(isIncomplete) + + fatalIf(err, "Error with client stat") + + // piping all content into the object channel to be processed + go func() { + defer close(objectsCh) + objectsCh <- content + }() + + // get content from channel to be converted into presigned URL + for content := range objectsCh { + fatalIf(content.Err, "Error with content") + + if content.Type.IsDir() { + continue + } + + objectURL := content.URL.String() + newClnt, err := newClientFromAlias(targetAlias, objectURL) + fatalIf(err, "Error with newClientFromAlias") + + // set default expiry for each url (point of no longer valid), to be 7 days + expiry := time.Duration(604800) * time.Second + shareURL, err := newClnt.ShareDownload(expiry) + fatalIf(err, "Error with ShareDownloa") + + return shareURL + } + return "" +} diff --git a/cmd/find_test.go b/cmd/find_test.go new file mode 100644 index 0000000000..317cef55eb --- /dev/null +++ b/cmd/find_test.go @@ -0,0 +1,79 @@ +/* + * Minio Client (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "testing" +) + +//TestFind is the structure used to contain params pertinent to find related tests +type TestFind struct { + pattern, filePath, flagName string + match bool +} + +var basicTests = []TestFind{ + //basic name and path tests + {"*.jpg", "carter.jpg", "name", true}, + {"*.jpg", "carter.jpeg", "name", false}, + {"*/test/*", "/test/bob/likes/cake", "name", false}, + {"*/test/*", "/test/bob/likes/cake", "path", true}, + {"*test/*", "bob/test/likes/cake", "name", false}, + {"*/test/*", "bob/test/likes/cake", "path", true}, + {"*test/*", "bob/likes/test/cake", "name", false}, + + //more advanced name and path tests + {"*/test/*", "bob/likes/cake/test", "name", false}, + {"*.jpg", ".jpg/elves/are/evil", "name", false}, + {"*.jpg", ".jpg/elves/are/evil", "path", false}, + {"*/test/*", "test1/test2/test3/test", "path", false}, + {"*/ test /*", "test/test1/test2/test3/test", "path", false}, + {"*/test/*", " test /I/have/Really/Long/hair", "path", false}, + {"*XA==", "I/enjoy/morning/walks/XA==", "name ", true}, + {"*XA==", "XA==/Height/is/a/social/construct", "path", false}, + {"*W", "/Word//this/is a/trickyTest", "path", false}, + {"*parser", "/This/might/mess up./the/parser", "name", true}, + {"*", "/bla/bla/bla/ ", "name", true}, + {"*LTIxNDc0ODM2NDgvLTE=", "What/A/Naughty/String/LTIxNDc0ODM2NDgvLTE=", "name", true}, + {"LTIxNDc0ODM2NDgvLTE=", "LTIxNDc0ODM2NDgvLTE=/I/Am/One/Baaaaad/String", "path", false}, + {"wq3YgNiB2ILYg9iE2IXYnNud3I/hoI7igIvigIzigI3igI7igI/igKrigKvigKzigK3igK7igaDi", "An/Even/Bigger/String/wq3YgNiB2ILYg9iE2IXYnNud3I/hoI7igIvigIzigI3igI7igI/igKrigKvigKzigK3igK7igaDi", "name", false}, + {"/", "funky/path/name", "path", false}, + {"๐•ฟ๐–๐–Š", "well/this/isAN/odd/font/THE", "name", false}, + {"๐•ฟ๐–๐–Š", "well/this/isAN/odd/font/The", "name", false}, + {"๐•ฟ๐–๐–Š", "well/this/isAN/odd/font/๐“ฃ๐“ฑ๐“ฎ", "name", false}, + {"๐•ฟ๐–๐–Š", "what/a/strange/turn/of/events/๐“ฃhe", "name", false}, + {"๐•ฟ๐–๐–Š", "well/this/isAN/odd/font/๐•ฟ๐–๐–Š", "name", true}, + + //implement some tests of regex + +} + +func TestFindMethod(t *testing.T) { + for _, test := range basicTests { + switch test.flagName { + case "name": + if testMatch, _ := nameMatch(test.filePath, test.pattern); testMatch != test.match { + t.Fatalf("Unexpected result %t, with pattern %s, flag %s and filepath %s \n", !test.match, test.pattern, test.flagName, test.filePath) + } + case "path": + if testMatch := pathMatch(test.filePath, test.pattern); testMatch != test.match { + t.Fatalf("Unexpected result %t, with pattern %s, flag %s and filepath %s \n", !test.match, test.pattern, test.flagName, test.filePath) + } + + } + } +} diff --git a/cmd/main.go b/cmd/main.go index 14852db8fa..04d9b5548f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -286,6 +286,7 @@ func registerApp() *cli.App { registerCmd(configCmd) // Configure minio client. registerCmd(updateCmd) // Check for new software updates. registerCmd(versionCmd) // Print version. + registerCmd(findCmd) // Find specific String patterns cli.HelpFlag = cli.BoolFlag{ Name: "help, h", diff --git a/vendor/github.com/minio/minio/pkg/wildcard/match.go b/vendor/github.com/minio/minio/pkg/wildcard/match.go new file mode 100644 index 0000000000..05726c79b8 --- /dev/null +++ b/vendor/github.com/minio/minio/pkg/wildcard/match.go @@ -0,0 +1,71 @@ +/* + * Minio Cloud Storage, (C) 2015, 2016 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package wildcard + +// MatchSimple - finds whether the text matches/satisfies the pattern string. +// supports only '*' wildcard in the pattern. +// considers a file system path as a flat name space. +func MatchSimple(pattern, name string) bool { + if pattern == "" { + return name == pattern + } + if pattern == "*" { + return true + } + rname := []rune(name) + rpattern := []rune(pattern) + simple := true // Does only wildcard '*' match. + return deepMatchRune(rname, rpattern, simple) +} + +// Match - finds whether the text matches/satisfies the pattern string. +// supports '*' and '?' wildcards in the pattern string. +// unlike path.Match(), considers a path as a flat name space while matching the pattern. +// The difference is illustrated in the example here https://play.golang.org/p/Ega9qgD4Qz . +func Match(pattern, name string) (matched bool) { + if pattern == "" { + return name == pattern + } + if pattern == "*" { + return true + } + rname := []rune(name) + rpattern := []rune(pattern) + simple := false // Does extended wildcard '*' and '?' match. + return deepMatchRune(rname, rpattern, simple) +} + +func deepMatchRune(str, pattern []rune, simple bool) bool { + for len(pattern) > 0 { + switch pattern[0] { + default: + if len(str) == 0 || str[0] != pattern[0] { + return false + } + case '?': + if len(str) == 0 && !simple { + return false + } + case '*': + return deepMatchRune(str, pattern[1:], simple) || + (len(str) > 0 && deepMatchRune(str[1:], pattern, simple)) + } + str = str[1:] + pattern = pattern[1:] + } + return len(str) == 0 && len(pattern) == 0 +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 0ad5f2f991..4ccf7a5e22 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -121,6 +121,12 @@ "revision": "8912b6bf3b22ff668b6448742cd50e7b837182ec", "revisionTime": "2017-02-10T19:51:41Z" }, + { + "checksumSHA1": "8g6v+ymjaYxCSzZc98CjSs7OXhM=", + "path": "github.com/minio/minio/pkg/wildcard", + "revision": "ec5293ce29ef03726c7174d9e3304bc156259c4e", + "revisionTime": "2017-07-24T19:46:37Z" + }, { "checksumSHA1": "+lLm3Uz721j8WzlV813lWiATrXM=", "path": "github.com/minio/minio/pkg/words",