mirror of
https://github.com/minio/mc.git
synced 2025-11-12 01:02:26 +03:00
444 lines
14 KiB
Go
444 lines
14 KiB
Go
/*
|
|
* 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 (
|
|
"bytes"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"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"
|
|
)
|
|
|
|
// findMessage holds JSON and string values for printing find command output.
|
|
type findMessage struct {
|
|
contentMessage
|
|
}
|
|
|
|
// String calls tells the console what to print and how to print it.
|
|
func (f findMessage) String() string {
|
|
return console.Colorize("Find", f.contentMessage.Key)
|
|
}
|
|
|
|
// JSON formats output to be JSON output.
|
|
func (f findMessage) JSON() string {
|
|
return f.contentMessage.JSON()
|
|
}
|
|
|
|
// nameMatch is similar to filepath.Match but only matches the
|
|
// base path of the input, if we couldn't find a match we
|
|
// also proceed to look for similar strings alone and print it.
|
|
//
|
|
// pattern:
|
|
// { term }
|
|
// term:
|
|
// '*' matches any sequence of non-Separator characters
|
|
// '?' matches any single non-Separator character
|
|
// '[' [ '^' ] { character-range } ']'
|
|
// character class (must be non-empty)
|
|
// c matches character c (c != '*', '?', '\\', '[')
|
|
// '\\' c matches character c
|
|
// character-range:
|
|
// c matches character c (c != '\\', '-', ']')
|
|
// '\\' c matches character c
|
|
// lo '-' hi matches character c for lo <= c <= hi
|
|
//
|
|
func nameMatch(pattern, path string) bool {
|
|
matched, e := filepath.Match(pattern, filepath.Base(path))
|
|
errorIf(probe.NewError(e).Trace(pattern, path), "Unable to match with input pattern.")
|
|
if !matched {
|
|
for _, pathComponent := range strings.Split(path, "/") {
|
|
matched = pathComponent == pattern
|
|
if matched {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return matched
|
|
}
|
|
|
|
// pathMatch reports whether path matches the wildcard pattern.
|
|
// 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 pathMatch(pattern, path string) bool {
|
|
return wildcard.Match(pattern, path)
|
|
}
|
|
|
|
// regexMatch reports whether path matches the regex pattern.
|
|
func regexMatch(pattern, path string) bool {
|
|
matched, e := regexp.MatchString(pattern, path)
|
|
errorIf(probe.NewError(e).Trace(pattern), "Unable to regex match with input pattern.")
|
|
return matched
|
|
}
|
|
|
|
func getExitStatus(err error) int {
|
|
if err == nil {
|
|
return 0
|
|
}
|
|
if pe, ok := err.(*exec.ExitError); ok {
|
|
if es, ok := pe.ProcessState.Sys().(syscall.WaitStatus); ok {
|
|
return es.ExitStatus()
|
|
}
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// execFind executes the input command line, additionally formats input
|
|
// for the command line in accordance with subsititution arguments.
|
|
func execFind(command string) {
|
|
commandArgs := strings.Split(command, " ")
|
|
|
|
cmd := exec.Command(commandArgs[0], commandArgs[1:]...)
|
|
var out bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
console.Print(console.Colorize("FindExecErr", stderr.String()))
|
|
// Return exit status of the command run
|
|
os.Exit(getExitStatus(err))
|
|
}
|
|
console.PrintC(out.String())
|
|
}
|
|
|
|
// watchFind - enables listening on the input path, listens for all file/object
|
|
// created actions. Asynchronously executes the input command line, also allows
|
|
// formatting for the command line in accordance with subsititution arguments.
|
|
func watchFind(ctx *findContext) {
|
|
// Watch is not enabled, return quickly.
|
|
if !ctx.watch {
|
|
return
|
|
}
|
|
params := watchParams{
|
|
recursive: true,
|
|
events: []string{"put"},
|
|
}
|
|
watchObj, err := ctx.clnt.Watch(params)
|
|
fatalIf(err.Trace(ctx.targetAlias), "Cannot watch with given params.")
|
|
|
|
// Enables users to kill using the control + c
|
|
trapCh := signalTrap(os.Interrupt, syscall.SIGTERM)
|
|
|
|
// Loop until user CTRL-C the command line.
|
|
for {
|
|
select {
|
|
case <-trapCh:
|
|
console.Println()
|
|
close(watchObj.doneChan)
|
|
return
|
|
case event, ok := <-watchObj.Events():
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
time, e := time.Parse(time.RFC3339, event.Time)
|
|
if e != nil {
|
|
errorIf(probe.NewError(e).Trace(event.Time), "Unable to parse event time.")
|
|
continue
|
|
}
|
|
|
|
find(ctx, contentMessage{
|
|
Key: getAliasedPath(ctx, event.Path),
|
|
Time: time,
|
|
Size: event.Size,
|
|
})
|
|
|
|
case err, ok := <-watchObj.Errors():
|
|
if !ok {
|
|
return
|
|
}
|
|
errorIf(err, "Unable to watch for events.")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Descend at most (a non-negative integer) levels of files
|
|
// below the starting-prefix and trims the suffix. This function
|
|
// returns path as is without manipulation if the maxDepth is 0
|
|
// i.e (not set).
|
|
func trimSuffixAtMaxDepth(startPrefix, path, separator string, maxDepth uint) string {
|
|
if maxDepth == 0 {
|
|
return path
|
|
}
|
|
// Remove the requested prefix from consideration, maxDepth is
|
|
// only considered for all other levels excluding the starting prefix.
|
|
path = strings.TrimPrefix(path, startPrefix)
|
|
pathComponents := strings.SplitAfter(path, separator)
|
|
if len(pathComponents) >= int(maxDepth) {
|
|
pathComponents = pathComponents[:maxDepth]
|
|
}
|
|
pathComponents = append([]string{startPrefix}, pathComponents...)
|
|
return strings.Join(pathComponents, "")
|
|
}
|
|
|
|
// Get aliased path used finally in printing, trim paths to ensure
|
|
// that we have removed the fully qualified paths and original
|
|
// start prefix (targetAlias) is retained. This function also honors
|
|
// maxDepth if set then the resultant path will be trimmed at requested
|
|
// maxDepth.
|
|
func getAliasedPath(ctx *findContext, path string) string {
|
|
separator := string(ctx.clnt.GetURL().Separator)
|
|
prefixPath := ctx.clnt.GetURL().String()
|
|
var aliasedPath string
|
|
if ctx.targetAlias != "" {
|
|
aliasedPath = ctx.targetAlias + strings.TrimPrefix(path, strings.TrimSuffix(ctx.targetFullURL, separator))
|
|
} else {
|
|
aliasedPath = path
|
|
// look for prefix path, if found filter at that, Watch calls
|
|
// for example always provide absolute path. So for relative
|
|
// prefixes we need to employ this kind of code.
|
|
if i := strings.Index(path, prefixPath); i > 0 {
|
|
aliasedPath = path[i:]
|
|
}
|
|
}
|
|
return trimSuffixAtMaxDepth(ctx.targetURL, aliasedPath, separator, ctx.maxDepth)
|
|
}
|
|
|
|
func find(ctx *findContext, fileContent contentMessage) {
|
|
// Match the incoming content, didn't match return.
|
|
if !matchFind(ctx, fileContent) {
|
|
return
|
|
} // For all matching content
|
|
|
|
// proceed to either exec, format the output string.
|
|
if ctx.execCmd != "" {
|
|
execFind(stringsReplace(ctx.execCmd, fileContent))
|
|
return
|
|
}
|
|
if ctx.printFmt != "" {
|
|
fileContent.Key = stringsReplace(ctx.printFmt, fileContent)
|
|
}
|
|
printMsg(findMessage{fileContent})
|
|
}
|
|
|
|
// doFind - find is main function body which interprets and executes
|
|
// all the input parameters.
|
|
func doFind(ctx *findContext) error {
|
|
// If watch is enabled we will wait on the prefix perpetually
|
|
// for all I/O events until canceled by user, if watch is not enabled
|
|
// following defer is a no-op.
|
|
defer watchFind(ctx)
|
|
|
|
var prevKeyName string
|
|
|
|
// iterate over all content which is within the given directory
|
|
for content := range ctx.clnt.List(true, false, DirNone) {
|
|
if content.Err != nil {
|
|
switch content.Err.ToGoError().(type) {
|
|
// handle this specifically for filesystem related errors.
|
|
case BrokenSymlink:
|
|
errorIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list broken link.")
|
|
continue
|
|
case TooManyLevelsSymlink:
|
|
errorIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list too many levels link.")
|
|
continue
|
|
case PathNotFound:
|
|
errorIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list folder.")
|
|
continue
|
|
case PathInsufficientPermission:
|
|
errorIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list folder.")
|
|
continue
|
|
case ObjectOnGlacier:
|
|
errorIf(content.Err.Trace(ctx.clnt.GetURL().String()), "")
|
|
continue
|
|
}
|
|
fatalIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list folder.")
|
|
continue
|
|
}
|
|
|
|
fileKeyName := getAliasedPath(ctx, content.URL.String())
|
|
fileContent := contentMessage{
|
|
Key: fileKeyName,
|
|
Time: content.Time.Local(),
|
|
Size: content.Size,
|
|
}
|
|
|
|
// Match the incoming content, didn't match return.
|
|
if !matchFind(ctx, fileContent) || prevKeyName == fileKeyName {
|
|
continue
|
|
} // For all matching content
|
|
|
|
prevKeyName = fileKeyName
|
|
|
|
// proceed to either exec, format the output string.
|
|
if ctx.execCmd != "" {
|
|
execFind(stringsReplace(ctx.execCmd, fileContent))
|
|
continue
|
|
}
|
|
if ctx.printFmt != "" {
|
|
fileContent.Key = stringsReplace(ctx.printFmt, fileContent)
|
|
}
|
|
|
|
printMsg(findMessage{fileContent})
|
|
}
|
|
|
|
// Success, notice watch will execute in defer only if enabled and this call
|
|
// will return after watch is canceled.
|
|
return nil
|
|
}
|
|
|
|
// stringsReplace - formats the string to remove {} and replace each
|
|
// with the appropriate argument
|
|
func stringsReplace(args string, fileContent contentMessage) string {
|
|
// replace all instances of {}
|
|
str := args
|
|
if strings.Contains(str, "{}") {
|
|
str = strings.Replace(str, "{}", fileContent.Key, -1)
|
|
}
|
|
|
|
// replace all instances of {""}
|
|
if strings.Contains(str, `{""}`) {
|
|
str = strings.Replace(str, `{""}`, strconv.Quote(fileContent.Key), -1)
|
|
}
|
|
|
|
// replace all instances of {base}
|
|
if strings.Contains(str, "{base}") {
|
|
str = strings.Replace(str, "{base}", filepath.Base(fileContent.Key), -1)
|
|
}
|
|
|
|
// replace all instances of {"base"}
|
|
if strings.Contains(str, `{"base"}`) {
|
|
str = strings.Replace(str, `{"base"}`, strconv.Quote(filepath.Base(fileContent.Key)), -1)
|
|
}
|
|
|
|
// replace all instances of {dir}
|
|
if strings.Contains(str, "{dir}") {
|
|
str = strings.Replace(str, "{dir}", filepath.Dir(fileContent.Key), -1)
|
|
}
|
|
|
|
// replace all instances of {"dir"}
|
|
if strings.Contains(str, `{"dir"}`) {
|
|
str = strings.Replace(str, `{"dir"}`, strconv.Quote(filepath.Dir(fileContent.Key)), -1)
|
|
}
|
|
|
|
// replace all instances of {size}
|
|
if strings.Contains(str, "{size}") {
|
|
str = strings.Replace(str, "{size}", humanize.IBytes(uint64(fileContent.Size)), -1)
|
|
}
|
|
|
|
// replace all instances of {"size"}
|
|
if strings.Contains(str, `{"size"}`) {
|
|
str = strings.Replace(str, `{"size"}`, strconv.Quote(humanize.IBytes(uint64(fileContent.Size))), -1)
|
|
}
|
|
|
|
// replace all instances of {time}
|
|
if strings.Contains(str, "{time}") {
|
|
str = strings.Replace(str, "{time}", fileContent.Time.Format(printDate), -1)
|
|
}
|
|
|
|
// replace all instances of {"time"}
|
|
if strings.Contains(str, `{"time"}`) {
|
|
str = strings.Replace(str, `{"time"}`, strconv.Quote(fileContent.Time.Format(printDate)), -1)
|
|
}
|
|
|
|
// replace all instances of {url}
|
|
if strings.Contains(str, "{url}") {
|
|
str = strings.Replace(str, "{url}", getShareURL(fileContent.Key), -1)
|
|
}
|
|
|
|
// replace all instances of {"url"}
|
|
if strings.Contains(str, `{"url"}`) {
|
|
str = strings.Replace(str, `{"url"}`, strconv.Quote(getShareURL(fileContent.Key)), -1)
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
// matchFind matches whether fileContent matches appropriately with standard
|
|
// "pattern matching" flags requested by the user, such as "name", "path", "regex" ..etc.
|
|
func matchFind(ctx *findContext, fileContent contentMessage) (match bool) {
|
|
match = true
|
|
prefixPath := ctx.targetURL
|
|
// Add separator only if targetURL doesn't already have separator.
|
|
if !strings.HasPrefix(prefixPath, string(ctx.clnt.GetURL().Separator)) {
|
|
prefixPath = ctx.targetURL + string(ctx.clnt.GetURL().Separator)
|
|
}
|
|
// Trim the prefix such that we will apply file path matching techniques
|
|
// on path excluding the starting prefix.
|
|
path := strings.TrimPrefix(fileContent.Key, prefixPath)
|
|
if match && ctx.ignorePattern != "" {
|
|
match = !pathMatch(ctx.ignorePattern, path)
|
|
}
|
|
if match && ctx.namePattern != "" {
|
|
match = nameMatch(ctx.namePattern, path)
|
|
}
|
|
if match && ctx.pathPattern != "" {
|
|
match = pathMatch(ctx.pathPattern, path)
|
|
}
|
|
if match && ctx.regexPattern != "" {
|
|
match = regexMatch(ctx.regexPattern, path)
|
|
}
|
|
if match && ctx.olderThan != "" {
|
|
match = !isOlder(fileContent.Time, ctx.olderThan)
|
|
}
|
|
if match && ctx.newerThan != "" {
|
|
match = !isNewer(fileContent.Time, ctx.newerThan)
|
|
}
|
|
if match && ctx.largerSize > 0 {
|
|
match = int64(ctx.largerSize) < fileContent.Size
|
|
}
|
|
if match && ctx.smallerSize > 0 {
|
|
match = int64(ctx.smallerSize) > fileContent.Size
|
|
}
|
|
return match
|
|
}
|
|
|
|
// 7 days in seconds.
|
|
var defaultSevenDays = time.Duration(604800) * time.Second
|
|
|
|
// getShareURL is used in conjunction with the {url} substitution
|
|
// argument to generate and return presigned URLs, returns error if any.
|
|
func getShareURL(path string) string {
|
|
targetAlias, targetURLFull, _, err := expandAlias(path)
|
|
fatalIf(err.Trace(path), "Unable to expand alias.")
|
|
|
|
clnt, err := newClientFromAlias(targetAlias, targetURLFull)
|
|
fatalIf(err.Trace(targetAlias, targetURLFull), "Unable to initialize client instance from alias.")
|
|
|
|
content, err := clnt.Stat(false, false, nil)
|
|
fatalIf(err.Trace(targetURLFull, targetAlias), "Unable to lookup file/object.")
|
|
|
|
// Skip if its a directory.
|
|
if content.Type.IsDir() {
|
|
return ""
|
|
}
|
|
|
|
objectURL := content.URL.String()
|
|
newClnt, err := newClientFromAlias(targetAlias, objectURL)
|
|
fatalIf(err.Trace(targetAlias, objectURL), "Unable to initialize new client from alias.")
|
|
|
|
// Set default expiry for each url (point of no longer valid), to be 7 days
|
|
shareURL, err := newClnt.ShareDownload(defaultSevenDays)
|
|
fatalIf(err.Trace(targetAlias, objectURL), "Unable to generate share url.")
|
|
|
|
return shareURL
|
|
}
|