package service import ( "encoding/json" "fmt" "io/fs" "net/http" "os" "path/filepath" "strings" "time" "git.towk2.me/towk/makeshift/internal/archive" makeshift "git.towk2.me/towk/makeshift/pkg" "git.towk2.me/towk/makeshift/pkg/storage" "github.com/rs/zerolog/log" ) func (s *Service) Download() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( path = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/download") pluginNames = strings.Split(r.URL.Query().Get("plugins"), ",") profileIDs = strings.Split(r.URL.Query().Get("profiles"), ",") fileInfo os.FileInfo out *os.File store *storage.MemoryStorage = new(storage.MemoryStorage) hooks []makeshift.Hook contents []byte errs []error err error ) // initialize storage store.Init() log.Debug(). Str("path", path). Str("client_host", r.Host). Strs("plugins", pluginNames). Strs("profiles", profileIDs). Any("query", r.URL.Query()). Msg("Service.Download()") // prepare profiles errs = s.loadProfiles(profileIDs, store, errs) if len(errs) > 0 { log.Error().Errs("errs", errs).Msg("errors occurred loading profiles") errs = []error{} } // determine if path is directory, file, or exists if fileInfo, err = os.Stat(path); err == nil { if fileInfo.IsDir() { log.Debug(). Str("type", "directory"). Msg("Service.Download()") // get the final archive path archivePath := fmt.Sprintf("%d.tar.gz", time.Now().Unix()) out, err = os.Create(archivePath) if err != nil { s.writeErrorResponse(w, fmt.Sprintf("failed to create named file: %v", err), http.StatusInternalServerError) return } // get a list of filenames to archive filenamesToArchive := []string{} filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { if !d.IsDir() { filenamesToArchive = append(filenamesToArchive, path) } return nil }) log.Debug().Strs("files", filenamesToArchive).Send() // prepare plugins hooks, errs = s.loadPlugins(pluginNames, store, nil, errs) if len(errs) > 0 { log.Error().Errs("errs", errs).Msg("errors occurred loading plugins") errs = []error{} } // create an archive of the directory, run hooks, and download err = archive.Create(filenamesToArchive, out, hooks) if err != nil { s.writeErrorResponse(w, fmt.Sprintf("failed to create archive: %v", err.Error()), http.StatusInternalServerError) return } // load the final archive contents, err = os.ReadFile(archivePath) if err != nil { s.writeErrorResponse(w, fmt.Sprintf("failed to read archive contents: %v", err.Error()), http.StatusInternalServerError) return } w.Write(contents) // clean up the temporary archive err = os.Remove(archivePath) if err != nil { log.Error().Err(err).Msg("failed to remove temporary archive") return } } else { // download individual file log.Debug(). Str("type", "file"). Msg("Service.Download()") contents, err = os.ReadFile(path) if err != nil { s.writeErrorResponse(w, fmt.Sprintf("failed to read file to download: %v", err), http.StatusInternalServerError) return } // prepare plugins store.Set("file", contents) hooks, errs = s.loadPlugins(pluginNames, store, nil, errs) if len(errs) > 0 { log.Error().Errs("errs", errs).Msg("errors occurred loading plugins") errs = []error{} } if len(hooks) > 0 { // run pre-hooks to modify the contents of the file before archiving log.Debug().Int("hook_count", len(hooks)).Msg("running hooks") for _, hook := range hooks { log.Debug().Any("hook", map[string]any{ "store": hook.Data, "args": hook.Args, "plugin": map[string]string{ "name": hook.Plugin.Name(), "description": hook.Plugin.Description(), "version": hook.Plugin.Version(), }, }).Send() err = hook.Run() if err != nil { log.Error().Err(err).Str("plugin", hook.Plugin.Name()).Msg("failed to run plugin") continue } } // take the contents from the last hook and update files var ( hook = hooks[len(hooks)-1] data any ) data, err = hook.Data.Get("out") if err != nil { s.writeErrorResponse(w, fmt.Sprintf("failed to get data from hook: %v", err), http.StatusInternalServerError) return } w.Write([]byte(data.(string))) } else { w.Write(contents) } } } else { s.writeErrorResponse(w, err.Error(), http.StatusBadRequest) return } } } func (s *Service) Upload() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { } } func (s *Service) List() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( path = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/list") entries []string body []byte err error ) // show what we're listing log.Debug().Str("path", path).Msg("Service.List()") // walk directory and show all entries "ls" err = filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } entries = append(entries, d.Name()) return nil }) if err != nil { switch err { case fs.ErrNotExist, fs.ErrInvalid: http.Error(w, "No such file or directory...", http.StatusBadRequest) case fs.ErrPermission: http.Error(w, "Invalid permissions...", http.StatusForbidden) default: http.Error(w, "Something went wrong (file or directory *probably* does not exist)...", http.StatusInternalServerError) } return } body, err = json.Marshal(entries) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(body) } } func (s *Service) GetStatus(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(map[string]any{ "code": http.StatusOK, "message": "Configurator is healthy", }) if err != nil { fmt.Printf("failed to encode JSON response body: %v\n", err) return } } func (s *Service) loadProfiles(profileIDs []string, store storage.KVStore, errs []error) []error { // load data from profiles into the data store var profiles = make(makeshift.ProfileMap, len(profileIDs)) for i, profileID := range profileIDs { var ( profilePath = s.PathForProfileWithID(profileID) profile *makeshift.Profile err error ) if i > DEFAULT_PROFILES_MAX_COUNT { log.Warn().Msg("max profiles count reached...stopping") return errs } if profileID == "" { log.Warn().Msg("profile ID is empty...skipping") continue } log.Debug(). Str("id", profileID). Str("path", profilePath). Msg("load profile") profile, err = LoadProfileFromFile(profilePath) if err != nil { errs = append(errs, err) continue } profiles[profileID] = profile } store.Set("profiles", profiles) return errs } func (s *Service) loadPlugins(pluginNames []string, store storage.KVStore, args []string, errs []error) ([]makeshift.Hook, []error) { // create hooks to run from provided plugins specified var hooks []makeshift.Hook for i, pluginName := range pluginNames { var ( pluginPath string = s.PathForPluginWithName(pluginName) plugin makeshift.Plugin err error ) if i > DEFAULT_PLUGINS_MAX_COUNT { log.Warn().Msg("max plugins count reached...stopping") return hooks, errs } if pluginName == "" { log.Warn().Msg("plugin name is empty...skipping") continue } log.Debug(). Str("name", pluginName). Str("path", pluginPath). Msg("load plugin") // load the plugin from disk plugin, err = LoadPluginFromFile(pluginPath) if err != nil { errs = append(errs, err) continue } hooks = append(hooks, makeshift.Hook{ Data: store, Args: args, Plugin: plugin, }) } return hooks, errs }