@@ -7,10 +7,14 @@ import (
77 "compress/gzip"
88 "fmt"
99 "io"
10+ "io/fs"
1011 "os"
1112 "path/filepath"
1213
14+ "github.com/nlepage/go-tarfs"
15+
1316 "tidbyt.dev/pixlet/manifest"
17+ "tidbyt.dev/pixlet/runtime"
1418)
1519
1620const (
@@ -26,16 +30,12 @@ const (
2630
2731// AppBundle represents the unpacked bundle in our system.
2832type AppBundle struct {
29- Source []byte
3033 Manifest * manifest.Manifest
34+ Source fs.FS
3135}
3236
33- // InitFromPath translates a directory containing an app manifest and source
34- // into an AppBundle.
35- func InitFromPath (dir string ) (* AppBundle , error ) {
36- // Load manifest
37- path := filepath .Join (dir , manifest .ManifestFileName )
38- m , err := os .Open (path )
37+ func FromFS (fs fs.FS ) (* AppBundle , error ) {
38+ m , err := fs .Open (manifest .ManifestFileName )
3939 if err != nil {
4040 return nil , fmt .Errorf ("could not open manifest: %w" , err )
4141 }
@@ -46,80 +46,39 @@ func InitFromPath(dir string) (*AppBundle, error) {
4646 return nil , fmt .Errorf ("could not load manifest: %w" , err )
4747 }
4848
49- // Load source
50- path = filepath .Join (dir , man .FileName )
51- s , err := os .Open (path )
52- if err != nil {
53- return nil , fmt .Errorf ("could not open app source: %w" , err )
54- }
55- defer s .Close ()
56-
57- src , err := io .ReadAll (s )
58- if err != nil {
59- return nil , fmt .Errorf ("could not read app source: %w" , err )
60- }
61-
6249 // Create app bundle struct
6350 return & AppBundle {
6451 Manifest : man ,
65- Source : src ,
52+ Source : fs ,
6653 }, nil
6754}
6855
56+ // FromDir translates a directory containing an app manifest and source
57+ // into an AppBundle.
58+ func FromDir (dir string ) (* AppBundle , error ) {
59+ return FromFS (os .DirFS (dir ))
60+ }
61+
6962// LoadBundle loads a compressed archive into an AppBundle.
7063func LoadBundle (in io.Reader ) (* AppBundle , error ) {
7164 gzr , err := gzip .NewReader (in )
7265 if err != nil {
73- return nil , fmt .Errorf ("could not create gzip reader: %w" , err )
66+ return nil , fmt .Errorf ("creating gzip reader: %w" , err )
7467 }
7568 defer gzr .Close ()
7669
77- tr := tar .NewReader (gzr )
78- ab := & AppBundle {}
79-
80- for {
81- header , err := tr .Next ()
70+ // read the entire tarball into memory so that we can seek
71+ // around it, and so that the underlying reader can be closed.
72+ var b bytes.Buffer
73+ io .Copy (& b , gzr )
8274
83- switch {
84- case err == io .EOF :
85- // If there are no more files in the bundle, validate and return it.
86- if ab .Manifest == nil {
87- return nil , fmt .Errorf ("could not find manifest in archive" )
88- }
89- if ab .Source == nil {
90- return nil , fmt .Errorf ("could not find source in archive" )
91- }
92- return ab , nil
93- case err != nil :
94- // If there is an error, return immediately.
95- return nil , fmt .Errorf ("could not read archive: %w" , err )
96- case header == nil :
97- // If for some reason we end up with a blank header, continue to the
98- // next one.
99- continue
100- case header .Name == AppSourceName :
101- // Load the app source.
102- buff := make ([]byte , header .Size )
103- _ , err := io .ReadFull (tr , buff )
104- if err != nil {
105- return nil , fmt .Errorf ("could not read source from archive: %w" , err )
106- }
107- ab .Source = buff
108- case header .Name == manifest .ManifestFileName :
109- // Load the app manifest.
110- buff := make ([]byte , header .Size )
111- _ , err := io .ReadFull (tr , buff )
112- if err != nil {
113- return nil , fmt .Errorf ("could not read manifest from archive: %w" , err )
114- }
115-
116- man , err := manifest .LoadManifest (bytes .NewReader (buff ))
117- if err != nil {
118- return nil , fmt .Errorf ("could not load manifest: %w" , err )
119- }
120- ab .Manifest = man
121- }
75+ r := bytes .NewReader (b .Bytes ())
76+ fs , err := tarfs .New (r )
77+ if err != nil {
78+ return nil , fmt .Errorf ("creating tarfs: %w" , err )
12279 }
80+
81+ return FromFS (fs )
12382}
12483
12584// WriteBundleToPath is a helper to be able to write the bundle to a provided
@@ -137,6 +96,16 @@ func (b *AppBundle) WriteBundleToPath(dir string) error {
13796
13897// WriteBundle writes a compressed archive to the provided writer.
13998func (ab * AppBundle ) WriteBundle (out io.Writer ) error {
99+ // we don't want to naively write the entire source FS to the tarball,
100+ // since it could contain a lot of extraneous files. instead, run the
101+ // applet and interrogate it for the files it needs to include in the
102+ // bundle.
103+ app , err := runtime .NewAppletFromFS (ab .Manifest .ID , ab .Source , runtime .WithPrintDisabled ())
104+ if err != nil {
105+ return fmt .Errorf ("loading applet for bundling: %w" , err )
106+ }
107+ bundleFiles := app .PathsForBundle ()
108+
140109 // Setup writers.
141110 gzw := gzip .NewWriter (out )
142111 defer gzw .Close ()
@@ -146,7 +115,7 @@ func (ab *AppBundle) WriteBundle(out io.Writer) error {
146115
147116 // Write manifest.
148117 buff := & bytes.Buffer {}
149- err : = ab .Manifest .WriteManifest (buff )
118+ err = ab .Manifest .WriteManifest (buff )
150119 if err != nil {
151120 return fmt .Errorf ("could not write manifest to buffer: %w" , err )
152121 }
@@ -166,19 +135,37 @@ func (ab *AppBundle) WriteBundle(out io.Writer) error {
166135 return fmt .Errorf ("could not write manifest to archive: %w" , err )
167136 }
168137
169- // Write source.
170- hdr = & tar.Header {
171- Name : AppSourceName ,
172- Mode : 0600 ,
173- Size : int64 (len (ab .Source )),
174- }
175- err = tw .WriteHeader (hdr )
176- if err != nil {
177- return fmt .Errorf ("could not write source header: %w" , err )
178- }
179- _ , err = tw .Write (ab .Source )
180- if err != nil {
181- return fmt .Errorf ("could not write source to archive: %w" , err )
138+ // write sources.
139+ for _ , path := range bundleFiles {
140+ stat , err := fs .Stat (ab .Source , path )
141+ if err != nil {
142+ return fmt .Errorf ("could not stat %s: %w" , path , err )
143+ }
144+
145+ hdr , err := tar .FileInfoHeader (stat , "" )
146+ if err != nil {
147+ return fmt .Errorf ("creating header for %s: %w" , path , err )
148+ }
149+ hdr .Name = filepath .ToSlash (path )
150+
151+ err = tw .WriteHeader (hdr )
152+ if err != nil {
153+ return fmt .Errorf ("writing header for %s: %w" , path , err )
154+ }
155+
156+ if ! stat .IsDir () {
157+ file , err := ab .Source .Open (path )
158+ if err != nil {
159+ return fmt .Errorf ("opening file %s: %w" , path , err )
160+ }
161+
162+ written , err := io .Copy (tw , file )
163+ if err != nil {
164+ return fmt .Errorf ("writing file %s: %w" , path , err )
165+ } else if written != stat .Size () {
166+ return fmt .Errorf ("did not write entire file %s: %w" , path , err )
167+ }
168+ }
182169 }
183170
184171 return nil
0 commit comments