index.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. package tar2go
  2. import (
  3. "archive/tar"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "io/fs"
  8. "sync"
  9. )
  10. var (
  11. // ErrDelete should be returned by an UpdaterFn when the file should be deleted.
  12. ErrDelete = errors.New("delete")
  13. )
  14. // Index is a tar index that can be used to read files from a tar.
  15. type Index struct {
  16. rdr *io.SectionReader
  17. tar *tar.Reader
  18. mu sync.Mutex
  19. idx map[string]*indexReader
  20. }
  21. // NewIndex creates a new Index from the passed in io.ReaderAt.
  22. func NewIndex(rdr io.ReaderAt) *Index {
  23. ras, ok := rdr.(ReaderAtSized)
  24. var size int64
  25. if !ok {
  26. size = 1<<63 - 1
  27. } else {
  28. size = ras.Size()
  29. }
  30. return &Index{
  31. rdr: io.NewSectionReader(rdr, 0, size),
  32. idx: make(map[string]*indexReader),
  33. }
  34. }
  35. func (i *Index) indexWithLock(name string) (*indexReader, error) {
  36. i.mu.Lock()
  37. defer i.mu.Unlock()
  38. return i.index(name)
  39. }
  40. func filterFSPrefix(name string) string {
  41. if len(name) <= 1 {
  42. return name
  43. }
  44. if name[0] == '/' {
  45. return name[1:]
  46. }
  47. if len(name) > 2 && name[0] == '.' && name[1] == '/' {
  48. return name[2:]
  49. }
  50. return name
  51. }
  52. // This function must be called with the lock held.
  53. func (i *Index) index(name string) (*indexReader, error) {
  54. name = filterFSPrefix(name)
  55. if rdr, ok := i.idx[name]; ok {
  56. return rdr, nil
  57. }
  58. if i.tar == nil {
  59. i.tar = tar.NewReader(i.rdr)
  60. }
  61. for {
  62. hdr, err := i.tar.Next()
  63. if err != nil {
  64. if err == io.EOF {
  65. return nil, fs.ErrNotExist
  66. }
  67. return nil, fmt.Errorf("error indexing tar: %w", err)
  68. }
  69. pos, err := i.rdr.Seek(0, io.SeekCurrent)
  70. if err != nil {
  71. return nil, fmt.Errorf("error getting file offset: %w", err)
  72. }
  73. rdr := &indexReader{rdr: i.rdr, offset: pos, size: hdr.Size, hdr: hdr}
  74. hdrName := filterFSPrefix(hdr.Name)
  75. i.idx[hdrName] = rdr
  76. if hdrName == name {
  77. return rdr, nil
  78. }
  79. }
  80. }
  81. // Reader returns an io.ReaderAt that can be used to read the entire tar.
  82. func (i *Index) Reader() *io.SectionReader {
  83. return io.NewSectionReader(i.rdr, 0, i.rdr.Size())
  84. }
  85. // FS returns an fs.FS that can be used to read the files in the tar.
  86. func (i *Index) FS() fs.FS {
  87. return &filesystem{idx: i}
  88. }
  89. // ReaderAtSized is an io.ReaderAt that also implements a Size method.
  90. type ReaderAtSized interface {
  91. io.ReaderAt
  92. Size() int64
  93. }
  94. // UpdaterFn is a function that is passed the name of the file and a ReaderAtSized
  95. type UpdaterFn func(string, ReaderAtSized) (ReaderAtSized, bool, error)
  96. // Replace replaces the file with the passed in name with the passed in ReaderAtSized.
  97. // If the passed in ReaderAtSized is nil, the file will be deleted.
  98. // If the file does not exist, it will be added.
  99. //
  100. // This function does not update the actual tar file, it only updates the index.
  101. func (i *Index) Replace(name string, rdr ReaderAtSized) error {
  102. i.mu.Lock()
  103. defer i.mu.Unlock()
  104. // index may overwrite it this replacement.
  105. i.index(name)
  106. if rdr == nil {
  107. delete(i.idx, name)
  108. return nil
  109. }
  110. i.idx[name] = &indexReader{rdr: rdr, offset: 0, size: rdr.Size(), hdr: &tar.Header{
  111. Name: name,
  112. Size: rdr.Size(),
  113. }}
  114. return nil
  115. }
  116. // Update creates a new tar with the files updated by the passed in updater function.
  117. // The output tar is written to the passed in io.Writer
  118. func (i *Index) Update(w io.Writer, updater UpdaterFn) error {
  119. tw := tar.NewWriter(w)
  120. defer tw.Close()
  121. rdr := i.Reader()
  122. tr := tar.NewReader(rdr)
  123. for {
  124. hdr, err := tr.Next()
  125. if err != nil {
  126. if err == io.EOF {
  127. return nil
  128. }
  129. return fmt.Errorf("error reading tar: %w", err)
  130. }
  131. offset, err := rdr.Seek(0, io.SeekCurrent)
  132. if err != nil {
  133. return fmt.Errorf("error getting file offset: %w", err)
  134. }
  135. ra, updated, err := updater(hdr.Name, io.NewSectionReader(i.rdr, offset, hdr.Size))
  136. if err != nil {
  137. if err == ErrDelete {
  138. continue
  139. }
  140. return fmt.Errorf("error updating file %s: %w", hdr.Name, err)
  141. }
  142. if updated {
  143. hdr.Size = ra.Size()
  144. }
  145. if err := tw.WriteHeader(hdr); err != nil {
  146. return fmt.Errorf("error writing tar header: %w", err)
  147. }
  148. if _, err := io.Copy(tw, io.NewSectionReader(ra, 0, ra.Size())); err != nil {
  149. return fmt.Errorf("error writing tar file: %w", err)
  150. }
  151. }
  152. }
  153. type indexReader struct {
  154. rdr io.ReaderAt
  155. offset int64
  156. size int64
  157. hdr *tar.Header
  158. }
  159. func (r *indexReader) Reader() *io.SectionReader {
  160. return io.NewSectionReader(r.rdr, r.offset, r.size)
  161. }