diff --git a/taskfile/read.go b/taskfile/reader.go similarity index 77% rename from taskfile/read.go rename to taskfile/reader.go index cfe81110..081479b1 100644 --- a/taskfile/read.go +++ b/taskfile/reader.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "path/filepath" "time" "gopkg.in/yaml.v3" @@ -12,130 +11,10 @@ import ( "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/logger" - "github.com/go-task/task/v3/internal/sysinfo" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) -var ( - // ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs - ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile") - - defaultTaskfiles = []string{ - "Taskfile.yml", - "taskfile.yml", - "Taskfile.yaml", - "taskfile.yaml", - "Taskfile.dist.yml", - "taskfile.dist.yml", - "Taskfile.dist.yaml", - "taskfile.dist.yaml", - } -) - -func readTaskfile( - node Node, - download, - offline bool, - timeout time.Duration, - tempDir string, - l *logger.Logger, -) (*ast.Taskfile, error) { - var b []byte - var err error - var cache *Cache - - if node.Remote() { - cache, err = NewCache(tempDir) - if err != nil { - return nil, err - } - } - - // If the file is remote and we're in offline mode, check if we have a cached copy - if node.Remote() && offline { - if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) { - return nil, &errors.TaskfileCacheNotFound{URI: node.Location()} - } else if err != nil { - return nil, err - } - l.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location()) - - } else { - - downloaded := false - ctx, cf := context.WithTimeout(context.Background(), timeout) - defer cf() - - // Read the file - b, err = node.Read(ctx) - // If we timed out then we likely have a network issue - if node.Remote() && errors.Is(ctx.Err(), context.DeadlineExceeded) { - // If a download was requested, then we can't use a cached copy - if download { - return nil, &errors.TaskfileNetworkTimeout{URI: node.Location(), Timeout: timeout} - } - // Search for any cached copies - if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) { - return nil, &errors.TaskfileNetworkTimeout{URI: node.Location(), Timeout: timeout, CheckedCache: true} - } else if err != nil { - return nil, err - } - l.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location()) - } else if err != nil { - return nil, err - } else { - downloaded = true - } - - // If the node was remote, we need to check the checksum - if node.Remote() && downloaded { - l.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location()) - - // Get the checksums - checksum := checksum(b) - cachedChecksum := cache.readChecksum(node) - - var msg string - if cachedChecksum == "" { - // If the checksum doesn't exist, prompt the user to continue - msg = fmt.Sprintf("The task you are attempting to run depends on the remote Taskfile at %q.\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location()) - } else if checksum != cachedChecksum { - // If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue - msg = fmt.Sprintf("The Taskfile at %q has changed since you last used it!\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location()) - } - if msg != "" { - if err := l.Prompt(logger.Yellow, msg, "n", "y", "yes"); errors.Is(err, logger.ErrPromptCancelled) { - return nil, &errors.TaskfileNotTrustedError{URI: node.Location()} - } else if err != nil { - return nil, err - } - } - - // If the hash has changed (or is new) - if checksum != cachedChecksum { - // Store the checksum - if err := cache.writeChecksum(node, checksum); err != nil { - return nil, err - } - // Cache the file - l.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location()) - if err = cache.write(node, b); err != nil { - return nil, err - } - } - } - } - - var t ast.Taskfile - if err := yaml.Unmarshal(b, &t); err != nil { - return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} - } - t.Location = node.Location() - - return &t, nil -} - // Read reads a Read for a given directory // Uses current dir when dir is left empty. Uses Read.yml // or Read.yaml when entrypoint is left empty @@ -279,63 +158,107 @@ func Read( return _taskfile(node) } -// Exists will check if a file at the given path Exists. If it does, it will -// return the path to it. If it does not, it will search the search for any -// files at the given path with any of the default Taskfile files names. If any -// of these match a file, the first matching path will be returned. If no files -// are found, an error will be returned. -func Exists(path string) (string, error) { - fi, err := os.Stat(path) - if err != nil { - return "", err - } - if fi.Mode().IsRegular() { - return filepath.Abs(path) - } +func readTaskfile( + node Node, + download, + offline bool, + timeout time.Duration, + tempDir string, + l *logger.Logger, +) (*ast.Taskfile, error) { + var b []byte + var err error + var cache *Cache - for _, n := range defaultTaskfiles { - fpath := filepathext.SmartJoin(path, n) - if _, err := os.Stat(fpath); err == nil { - return filepath.Abs(fpath) - } - } - - return "", errors.TaskfileNotFoundError{URI: path, Walk: false} -} - -// ExistsWalk will check if a file at the given path exists by calling the -// exists function. If a file is not found, it will walk up the directory tree -// calling the exists function until it finds a file or reaches the root -// directory. On supported operating systems, it will also check if the user ID -// of the directory changes and abort if it does. -func ExistsWalk(path string) (string, error) { - origPath := path - owner, err := sysinfo.Owner(path) - if err != nil { - return "", err - } - for { - fpath, err := Exists(path) - if err == nil { - return fpath, nil - } - - // Get the parent path/user id - parentPath := filepath.Dir(path) - parentOwner, err := sysinfo.Owner(parentPath) + if node.Remote() { + cache, err = NewCache(tempDir) if err != nil { - return "", err + return nil, err } - - // Error if we reached the root directory and still haven't found a file - // OR if the user id of the directory changes - if path == parentPath || (parentOwner != owner) { - return "", errors.TaskfileNotFoundError{URI: origPath, Walk: false} - } - - owner = parentOwner - path = parentPath } + + // If the file is remote and we're in offline mode, check if we have a cached copy + if node.Remote() && offline { + if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) { + return nil, &errors.TaskfileCacheNotFound{URI: node.Location()} + } else if err != nil { + return nil, err + } + l.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location()) + + } else { + + downloaded := false + ctx, cf := context.WithTimeout(context.Background(), timeout) + defer cf() + + // Read the file + b, err = node.Read(ctx) + // If we timed out then we likely have a network issue + if node.Remote() && errors.Is(ctx.Err(), context.DeadlineExceeded) { + // If a download was requested, then we can't use a cached copy + if download { + return nil, &errors.TaskfileNetworkTimeout{URI: node.Location(), Timeout: timeout} + } + // Search for any cached copies + if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) { + return nil, &errors.TaskfileNetworkTimeout{URI: node.Location(), Timeout: timeout, CheckedCache: true} + } else if err != nil { + return nil, err + } + l.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location()) + } else if err != nil { + return nil, err + } else { + downloaded = true + } + + // If the node was remote, we need to check the checksum + if node.Remote() && downloaded { + l.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location()) + + // Get the checksums + checksum := checksum(b) + cachedChecksum := cache.readChecksum(node) + + var msg string + if cachedChecksum == "" { + // If the checksum doesn't exist, prompt the user to continue + msg = fmt.Sprintf("The task you are attempting to run depends on the remote Taskfile at %q.\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location()) + } else if checksum != cachedChecksum { + // If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue + msg = fmt.Sprintf("The Taskfile at %q has changed since you last used it!\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location()) + } + if msg != "" { + if err := l.Prompt(logger.Yellow, msg, "n", "y", "yes"); errors.Is(err, logger.ErrPromptCancelled) { + return nil, &errors.TaskfileNotTrustedError{URI: node.Location()} + } else if err != nil { + return nil, err + } + } + + // If the hash has changed (or is new) + if checksum != cachedChecksum { + // Store the checksum + if err := cache.writeChecksum(node, checksum); err != nil { + return nil, err + } + // Cache the file + l.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location()) + if err = cache.write(node, b); err != nil { + return nil, err + } + } + } + } + + var t ast.Taskfile + if err := yaml.Unmarshal(b, &t); err != nil { + return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} + } + t.Location = node.Location() + + return &t, nil } func checkCircularIncludes(node Node) error { diff --git a/taskfile/taskfile.go b/taskfile/taskfile.go new file mode 100644 index 00000000..f43a93f3 --- /dev/null +++ b/taskfile/taskfile.go @@ -0,0 +1,85 @@ +package taskfile + +import ( + "os" + "path/filepath" + + "github.com/go-task/task/v3/errors" + "github.com/go-task/task/v3/internal/filepathext" + "github.com/go-task/task/v3/internal/sysinfo" +) + +var ( + // ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs + ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile") + + defaultTaskfiles = []string{ + "Taskfile.yml", + "taskfile.yml", + "Taskfile.yaml", + "taskfile.yaml", + "Taskfile.dist.yml", + "taskfile.dist.yml", + "Taskfile.dist.yaml", + "taskfile.dist.yaml", + } +) + +// Exists will check if a file at the given path Exists. If it does, it will +// return the path to it. If it does not, it will search the search for any +// files at the given path with any of the default Taskfile files names. If any +// of these match a file, the first matching path will be returned. If no files +// are found, an error will be returned. +func Exists(path string) (string, error) { + fi, err := os.Stat(path) + if err != nil { + return "", err + } + if fi.Mode().IsRegular() { + return filepath.Abs(path) + } + + for _, n := range defaultTaskfiles { + fpath := filepathext.SmartJoin(path, n) + if _, err := os.Stat(fpath); err == nil { + return filepath.Abs(fpath) + } + } + + return "", errors.TaskfileNotFoundError{URI: path, Walk: false} +} + +// ExistsWalk will check if a file at the given path exists by calling the +// exists function. If a file is not found, it will walk up the directory tree +// calling the exists function until it finds a file or reaches the root +// directory. On supported operating systems, it will also check if the user ID +// of the directory changes and abort if it does. +func ExistsWalk(path string) (string, error) { + origPath := path + owner, err := sysinfo.Owner(path) + if err != nil { + return "", err + } + for { + fpath, err := Exists(path) + if err == nil { + return fpath, nil + } + + // Get the parent path/user id + parentPath := filepath.Dir(path) + parentOwner, err := sysinfo.Owner(parentPath) + if err != nil { + return "", err + } + + // Error if we reached the root directory and still haven't found a file + // OR if the user id of the directory changes + if path == parentPath || (parentOwner != owner) { + return "", errors.TaskfileNotFoundError{URI: origPath, Walk: false} + } + + owner = parentOwner + path = parentPath + } +}