taskfile.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package taskfile
  2. import (
  3. "context"
  4. "net/http"
  5. "net/url"
  6. "os"
  7. "path/filepath"
  8. "slices"
  9. "strings"
  10. "time"
  11. "github.com/go-task/task/v3/errors"
  12. "github.com/go-task/task/v3/internal/filepathext"
  13. "github.com/go-task/task/v3/internal/logger"
  14. "github.com/go-task/task/v3/internal/sysinfo"
  15. )
  16. var (
  17. defaultTaskfiles = []string{
  18. "Taskfile.yml",
  19. "taskfile.yml",
  20. "Taskfile.yaml",
  21. "taskfile.yaml",
  22. "Taskfile.dist.yml",
  23. "taskfile.dist.yml",
  24. "Taskfile.dist.yaml",
  25. "taskfile.dist.yaml",
  26. }
  27. allowedContentTypes = []string{
  28. "text/plain",
  29. "text/yaml",
  30. "text/x-yaml",
  31. "application/yaml",
  32. "application/x-yaml",
  33. }
  34. )
  35. // RemoteExists will check if a file at the given URL Exists. If it does, it
  36. // will return its URL. If it does not, it will search the search for any files
  37. // at the given URL with any of the default Taskfile files names. If any of
  38. // these match a file, the first matching path will be returned. If no files are
  39. // found, an error will be returned.
  40. func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout time.Duration) (*url.URL, error) {
  41. // Create a new HEAD request for the given URL to check if the resource exists
  42. req, err := http.NewRequest("HEAD", u.String(), nil)
  43. if err != nil {
  44. return nil, errors.TaskfileFetchFailedError{URI: u.String()}
  45. }
  46. // Request the given URL
  47. resp, err := http.DefaultClient.Do(req.WithContext(ctx))
  48. if err != nil {
  49. if errors.Is(ctx.Err(), context.DeadlineExceeded) {
  50. return nil, &errors.TaskfileNetworkTimeoutError{URI: u.String(), Timeout: timeout}
  51. }
  52. return nil, errors.TaskfileFetchFailedError{URI: u.String()}
  53. }
  54. defer resp.Body.Close()
  55. // If the request was successful and the content type is allowed, return the
  56. // URL The content type check is to avoid downloading files that are not
  57. // Taskfiles It means we can try other files instead of downloading
  58. // something that is definitely not a Taskfile
  59. contentType := resp.Header.Get("Content-Type")
  60. if resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool {
  61. return strings.Contains(contentType, s)
  62. }) {
  63. return u, nil
  64. }
  65. // If the request was not successful, append the default Taskfile names to
  66. // the URL and return the URL of the first successful request
  67. for _, taskfile := range defaultTaskfiles {
  68. // Fixes a bug with JoinPath where a leading slash is not added to the
  69. // path if it is empty
  70. if u.Path == "" {
  71. u.Path = "/"
  72. }
  73. alt := u.JoinPath(taskfile)
  74. req.URL = alt
  75. // Try the alternative URL
  76. resp, err = http.DefaultClient.Do(req)
  77. if err != nil {
  78. return nil, errors.TaskfileFetchFailedError{URI: u.String()}
  79. }
  80. defer resp.Body.Close()
  81. // If the request was successful, return the URL
  82. if resp.StatusCode == http.StatusOK {
  83. l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", alt.String(), taskfile)
  84. return alt, nil
  85. }
  86. }
  87. return nil, errors.TaskfileNotFoundError{URI: u.String(), Walk: false}
  88. }
  89. // Exists will check if a file at the given path Exists. If it does, it will
  90. // return the path to it. If it does not, it will search for any files at the
  91. // given path with any of the default Taskfile files names. If any of these
  92. // match a file, the first matching path will be returned. If no files are
  93. // found, an error will be returned.
  94. func Exists(l *logger.Logger, path string) (string, error) {
  95. fi, err := os.Stat(path)
  96. if err != nil {
  97. return "", err
  98. }
  99. if fi.Mode().IsRegular() ||
  100. fi.Mode()&os.ModeDevice != 0 ||
  101. fi.Mode()&os.ModeSymlink != 0 ||
  102. fi.Mode()&os.ModeNamedPipe != 0 {
  103. return filepath.Abs(path)
  104. }
  105. for _, taskfile := range defaultTaskfiles {
  106. alt := filepathext.SmartJoin(path, taskfile)
  107. if _, err := os.Stat(alt); err == nil {
  108. l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", path, taskfile)
  109. return filepath.Abs(alt)
  110. }
  111. }
  112. return "", errors.TaskfileNotFoundError{URI: path, Walk: false}
  113. }
  114. // ExistsWalk will check if a file at the given path exists by calling the
  115. // exists function. If a file is not found, it will walk up the directory tree
  116. // calling the exists function until it finds a file or reaches the root
  117. // directory. On supported operating systems, it will also check if the user ID
  118. // of the directory changes and abort if it does.
  119. func ExistsWalk(l *logger.Logger, path string) (string, error) {
  120. origPath := path
  121. owner, err := sysinfo.Owner(path)
  122. if err != nil {
  123. return "", err
  124. }
  125. for {
  126. fpath, err := Exists(l, path)
  127. if err == nil {
  128. return fpath, nil
  129. }
  130. // Get the parent path/user id
  131. parentPath := filepath.Dir(path)
  132. parentOwner, err := sysinfo.Owner(parentPath)
  133. if err != nil {
  134. return "", err
  135. }
  136. // Error if we reached the root directory and still haven't found a file
  137. // OR if the user id of the directory changes
  138. if path == parentPath || (parentOwner != owner) {
  139. return "", errors.TaskfileNotFoundError{URI: origPath, Walk: false}
  140. }
  141. owner = parentOwner
  142. path = parentPath
  143. }
  144. }