1
0

tar.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /*
  2. Copyright 2018 The Tilt Dev Authors
  3. Copyright 2023 Docker Compose CLI authors
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. */
  14. package sync
  15. import (
  16. "archive/tar"
  17. "bytes"
  18. "context"
  19. "fmt"
  20. "io"
  21. "io/fs"
  22. "os"
  23. "path"
  24. "path/filepath"
  25. "strings"
  26. "github.com/hashicorp/go-multierror"
  27. "github.com/pkg/errors"
  28. "github.com/compose-spec/compose-go/types"
  29. moby "github.com/docker/docker/api/types"
  30. "github.com/docker/docker/pkg/archive"
  31. )
  32. type archiveEntry struct {
  33. path string
  34. info os.FileInfo
  35. header *tar.Header
  36. }
  37. type LowLevelClient interface {
  38. ContainersForService(ctx context.Context, projectName string, serviceName string) ([]moby.Container, error)
  39. Exec(ctx context.Context, containerID string, cmd []string, in io.Reader) error
  40. }
  41. type Tar struct {
  42. client LowLevelClient
  43. projectName string
  44. }
  45. var _ Syncer = &Tar{}
  46. func NewTar(projectName string, client LowLevelClient) *Tar {
  47. return &Tar{
  48. projectName: projectName,
  49. client: client,
  50. }
  51. }
  52. func (t *Tar) Sync(ctx context.Context, service types.ServiceConfig, paths []PathMapping) error {
  53. containers, err := t.client.ContainersForService(ctx, t.projectName, service.Name)
  54. if err != nil {
  55. return err
  56. }
  57. var pathsToCopy []PathMapping
  58. var pathsToDelete []string
  59. for _, p := range paths {
  60. if _, err := os.Stat(p.HostPath); err != nil && errors.Is(err, fs.ErrNotExist) {
  61. pathsToDelete = append(pathsToDelete, p.ContainerPath)
  62. } else {
  63. pathsToCopy = append(pathsToCopy, p)
  64. }
  65. }
  66. var deleteCmd []string
  67. if len(pathsToDelete) != 0 {
  68. deleteCmd = append([]string{"rm", "-rf"}, pathsToDelete...)
  69. }
  70. copyCmd := []string{"tar", "-v", "-C", "/", "-x", "-f", "-"}
  71. var eg multierror.Group
  72. writers := make([]*io.PipeWriter, len(containers))
  73. for i := range containers {
  74. containerID := containers[i].ID
  75. r, w := io.Pipe()
  76. writers[i] = w
  77. eg.Go(func() error {
  78. if len(deleteCmd) != 0 {
  79. if err := t.client.Exec(ctx, containerID, deleteCmd, nil); err != nil {
  80. return fmt.Errorf("deleting paths in %s: %w", containerID, err)
  81. }
  82. }
  83. if err := t.client.Exec(ctx, containerID, copyCmd, r); err != nil {
  84. return fmt.Errorf("copying files to %s: %w", containerID, err)
  85. }
  86. return nil
  87. })
  88. }
  89. multiWriter := newLossyMultiWriter(writers...)
  90. tarReader := tarArchive(pathsToCopy)
  91. defer func() {
  92. _ = tarReader.Close()
  93. multiWriter.Close()
  94. }()
  95. _, err = io.Copy(multiWriter, tarReader)
  96. if err != nil {
  97. return err
  98. }
  99. multiWriter.Close()
  100. return eg.Wait().ErrorOrNil()
  101. }
  102. type ArchiveBuilder struct {
  103. tw *tar.Writer
  104. // A shared I/O buffer to help with file copying.
  105. copyBuf *bytes.Buffer
  106. }
  107. func NewArchiveBuilder(writer io.Writer) *ArchiveBuilder {
  108. tw := tar.NewWriter(writer)
  109. return &ArchiveBuilder{
  110. tw: tw,
  111. copyBuf: &bytes.Buffer{},
  112. }
  113. }
  114. func (a *ArchiveBuilder) Close() error {
  115. return a.tw.Close()
  116. }
  117. // ArchivePathsIfExist creates a tar archive of all local files in `paths`. It quietly skips any paths that don't exist.
  118. func (a *ArchiveBuilder) ArchivePathsIfExist(paths []PathMapping) error {
  119. // In order to handle overlapping syncs, we
  120. // 1) collect all the entries,
  121. // 2) de-dupe them, with last-one-wins semantics
  122. // 3) write all the entries
  123. //
  124. // It's not obvious that this is the correct behavior. A better approach
  125. // (that's more in-line with how syncs work) might ignore files in earlier
  126. // path mappings when we know they're going to be "synced" over.
  127. // There's a bunch of subtle product decisions about how overlapping path
  128. // mappings work that we're not sure about.
  129. var entries []archiveEntry
  130. for _, p := range paths {
  131. newEntries, err := a.entriesForPath(p.HostPath, p.ContainerPath)
  132. if err != nil {
  133. return fmt.Errorf("inspecting %q: %w", p.HostPath, err)
  134. }
  135. entries = append(entries, newEntries...)
  136. }
  137. entries = dedupeEntries(entries)
  138. for _, entry := range entries {
  139. err := a.writeEntry(entry)
  140. if err != nil {
  141. return fmt.Errorf("archiving %q: %w", entry.path, err)
  142. }
  143. }
  144. return nil
  145. }
  146. func (a *ArchiveBuilder) writeEntry(entry archiveEntry) error {
  147. pathInTar := entry.path
  148. header := entry.header
  149. if header.Typeflag != tar.TypeReg {
  150. // anything other than a regular file (e.g. dir, symlink) just needs the header
  151. if err := a.tw.WriteHeader(header); err != nil {
  152. return fmt.Errorf("writing %q header: %w", pathInTar, err)
  153. }
  154. return nil
  155. }
  156. file, err := os.Open(pathInTar)
  157. if err != nil {
  158. // In case the file has been deleted since we last looked at it.
  159. if os.IsNotExist(err) {
  160. return nil
  161. }
  162. return err
  163. }
  164. defer func() {
  165. _ = file.Close()
  166. }()
  167. // The size header must match the number of contents bytes.
  168. //
  169. // There is room for a race condition here if something writes to the file
  170. // after we've read the file size.
  171. //
  172. // For small files, we avoid this by first copying the file into a buffer,
  173. // and using the size of the buffer to populate the header.
  174. //
  175. // For larger files, we don't want to copy the whole thing into a buffer,
  176. // because that would blow up heap size. There is some danger that this
  177. // will lead to a spurious error when the tar writer validates the sizes.
  178. // That error will be disruptive but will be handled as best as we
  179. // can downstream.
  180. useBuf := header.Size < 5000000
  181. if useBuf {
  182. a.copyBuf.Reset()
  183. _, err = io.Copy(a.copyBuf, file)
  184. if err != nil && err != io.EOF {
  185. return fmt.Errorf("copying %q: %w", pathInTar, err)
  186. }
  187. header.Size = int64(len(a.copyBuf.Bytes()))
  188. }
  189. // wait to write the header until _after_ the file is successfully opened
  190. // to avoid generating an invalid tar entry that has a header but no contents
  191. // in the case the file has been deleted
  192. err = a.tw.WriteHeader(header)
  193. if err != nil {
  194. return fmt.Errorf("writing %q header: %w", pathInTar, err)
  195. }
  196. if useBuf {
  197. _, err = io.Copy(a.tw, a.copyBuf)
  198. } else {
  199. _, err = io.Copy(a.tw, file)
  200. }
  201. if err != nil && err != io.EOF {
  202. return fmt.Errorf("copying %q: %w", pathInTar, err)
  203. }
  204. // explicitly flush so that if the entry is invalid we will detect it now and
  205. // provide a more meaningful error
  206. if err := a.tw.Flush(); err != nil {
  207. return fmt.Errorf("finalizing %q: %w", pathInTar, err)
  208. }
  209. return nil
  210. }
  211. // tarPath writes the given source path into tarWriter at the given dest (recursively for directories).
  212. // e.g. tarring my_dir --> dest d: d/file_a, d/file_b
  213. // If source path does not exist, quietly skips it and returns no err
  214. func (a *ArchiveBuilder) entriesForPath(localPath, containerPath string) ([]archiveEntry, error) {
  215. localInfo, err := os.Stat(localPath)
  216. if err != nil {
  217. if os.IsNotExist(err) {
  218. return nil, nil
  219. }
  220. return nil, err
  221. }
  222. localPathIsDir := localInfo.IsDir()
  223. if localPathIsDir {
  224. // Make sure we can trim this off filenames to get valid relative filepaths
  225. if !strings.HasSuffix(localPath, string(filepath.Separator)) {
  226. localPath += string(filepath.Separator)
  227. }
  228. }
  229. containerPath = strings.TrimPrefix(containerPath, "/")
  230. result := make([]archiveEntry, 0)
  231. err = filepath.Walk(localPath, func(curLocalPath string, info os.FileInfo, err error) error {
  232. if err != nil {
  233. return fmt.Errorf("walking %q: %w", curLocalPath, err)
  234. }
  235. linkname := ""
  236. if info.Mode()&os.ModeSymlink != 0 {
  237. var err error
  238. linkname, err = os.Readlink(curLocalPath)
  239. if err != nil {
  240. return err
  241. }
  242. }
  243. var name string
  244. //nolint:gocritic
  245. if localPathIsDir {
  246. // Name of file in tar should be relative to source directory...
  247. tmp, err := filepath.Rel(localPath, curLocalPath)
  248. if err != nil {
  249. return fmt.Errorf("making %q relative to %q: %w", curLocalPath, localPath, err)
  250. }
  251. // ...and live inside `dest`
  252. name = path.Join(containerPath, filepath.ToSlash(tmp))
  253. } else if strings.HasSuffix(containerPath, "/") {
  254. name = containerPath + filepath.Base(curLocalPath)
  255. } else {
  256. name = containerPath
  257. }
  258. header, err := archive.FileInfoHeader(name, info, linkname)
  259. if err != nil {
  260. // Not all types of files are allowed in a tarball. That's OK.
  261. // Mimic the Docker behavior and just skip the file.
  262. return nil
  263. }
  264. result = append(result, archiveEntry{
  265. path: curLocalPath,
  266. info: info,
  267. header: header,
  268. })
  269. return nil
  270. })
  271. if err != nil {
  272. return nil, err
  273. }
  274. return result, nil
  275. }
  276. func tarArchive(ops []PathMapping) io.ReadCloser {
  277. pr, pw := io.Pipe()
  278. go func() {
  279. ab := NewArchiveBuilder(pw)
  280. err := ab.ArchivePathsIfExist(ops)
  281. if err != nil {
  282. _ = pw.CloseWithError(fmt.Errorf("adding files to tar: %w", err))
  283. } else {
  284. // propagate errors from the TarWriter::Close() because it performs a final
  285. // Flush() and any errors mean the tar is invalid
  286. if err := ab.Close(); err != nil {
  287. _ = pw.CloseWithError(fmt.Errorf("closing tar: %w", err))
  288. } else {
  289. _ = pw.Close()
  290. }
  291. }
  292. }()
  293. return pr
  294. }
  295. // Dedupe the entries with last-entry-wins semantics.
  296. func dedupeEntries(entries []archiveEntry) []archiveEntry {
  297. seenIndex := make(map[string]int, len(entries))
  298. result := make([]archiveEntry, 0, len(entries))
  299. for i, entry := range entries {
  300. seenIndex[entry.header.Name] = i
  301. }
  302. for i, entry := range entries {
  303. if seenIndex[entry.header.Name] == i {
  304. result = append(result, entry)
  305. }
  306. }
  307. return result
  308. }