package infinitime import ( "archive/zip" "encoding/json" "io" "os" "path/filepath" "go.arsenm.dev/infinitime/blefs" ) // ResourceOperation represents an operation performed during // resource loading type ResourceOperation uint8 const ( // ResourceOperationUpload represents the upload phase // of resource loading ResourceOperationUpload = iota // ResourceOperationRemoveObsolete represents the obsolete // file removal phase of resource loading ResourceOperationRemoveObsolete ) // ResourceManifest is the structure of the resource manifest file type ResourceManifest struct { Resources []Resource `json:"resources"` Obsolete []ObsoleteResource `json:"obsolete_files"` } // Resource represents a resource entry in the manifest type Resource struct { Name string `json:"filename"` Path string `json:"path"` } // ObsoleteResource represents an obsolete file entry in the manifest type ObsoleteResource struct { Path string `json:"path"` Since string `json:"since"` } // ResourceLoadProgress contains information on the progress of // a resource load type ResourceLoadProgress struct { Operation ResourceOperation Name string Total int64 Sent int64 Err error } // LoadResources accepts a resources zip file and a BLE FS. // It loads the resources from the zip onto the FS. func LoadResources(file *os.File, fs *blefs.FS) (<-chan ResourceLoadProgress, error) { out := make(chan ResourceLoadProgress, 10) fi, err := file.Stat() if err != nil { return nil, err } r, err := zip.NewReader(file, fi.Size()) if err != nil { return nil, err } m, err := r.Open("resources.json") if err != nil { return nil, err } defer m.Close() var manifest ResourceManifest err = json.NewDecoder(m).Decode(&manifest) if err != nil { return nil, err } m.Close() log.Debug().Msg("Decoded manifest file") go func() { defer close(out) for _, file := range manifest.Obsolete { name := filepath.Base(file.Path) log.Debug().Str("file", file.Path).Msg("Removing file") err := fs.RemoveAll(file.Path) if err != nil { out <- ResourceLoadProgress{ Name: name, Operation: ResourceOperationRemoveObsolete, Err: err, } return } log.Debug().Str("file", file.Path).Msg("Removed file") out <- ResourceLoadProgress{ Name: name, Operation: ResourceOperationRemoveObsolete, } } for _, file := range manifest.Resources { src, err := r.Open(file.Name) if err != nil { out <- ResourceLoadProgress{ Name: file.Name, Operation: ResourceOperationUpload, Err: err, } return } srcFi, err := src.Stat() if err != nil { out <- ResourceLoadProgress{ Name: file.Name, Operation: ResourceOperationUpload, Total: srcFi.Size(), Err: err, } src.Close() return } log.Debug().Str("file", file.Path).Msg("Making directories") err = fs.MkdirAll(filepath.Dir(file.Path)) if err != nil { log.Debug().Err(err).Msg("Error making directories") out <- ResourceLoadProgress{ Name: file.Name, Operation: ResourceOperationUpload, Total: srcFi.Size(), Err: err, } src.Close() return } log.Debug(). Str("file", file.Path). Int64("size", srcFi.Size()). Msg("Creating file") dst, err := fs.Create(file.Path, uint32(srcFi.Size())) if err != nil { log.Debug().Err(err).Msg("Error creating file") out <- ResourceLoadProgress{ Name: file.Name, Operation: ResourceOperationUpload, Total: srcFi.Size(), Err: err, } src.Close() return } progCh := dst.Progress() go func() { for sent := range progCh { log.Debug(). Int64("total", srcFi.Size()). Uint32("sent", sent). Msg("Progress event sent") out <- ResourceLoadProgress{ Name: file.Name, Operation: ResourceOperationUpload, Total: srcFi.Size(), Sent: int64(sent), } if sent == uint32(srcFi.Size()) { return } } }() n, err := io.Copy(dst, src) if err != nil { log.Debug().Err(err).Msg("Error writing to file") out <- ResourceLoadProgress{ Name: file.Name, Operation: ResourceOperationUpload, Total: srcFi.Size(), Sent: n, Err: err, } src.Close() dst.Close() return } src.Close() dst.Close() } }() return out, nil }