package service import ( "encoding/json" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "slices" "strings" "git.towk2.me/towk/makeshift/internal/archive" "git.towk2.me/towk/makeshift/internal/kwargs" makeshift "git.towk2.me/towk/makeshift/pkg" "git.towk2.me/towk/makeshift/pkg/storage" "git.towk2.me/towk/makeshift/pkg/util" "github.com/go-chi/chi/v5" "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") pluginKWArgs = chi.URLParam(r, "kwargs") pluginArgs = strings.Split(r.URL.Query().Get("args"), ",") pluginNames = strings.Split(r.URL.Query().Get("plugins"), ",") profileIDs = strings.Split(r.URL.Query().Get("profiles"), ",") kw *kwargs.KWArgs fileInfo os.FileInfo out *os.File store *storage.MemoryStorage = new(storage.MemoryStorage) hooks []makeshift.Hook contents []byte errs []error err error ) // parse the KWArgs from request kw.Set(pluginKWArgs) // initialize storage store.Init() store.SetKWArgs(kw) 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") err = util.FormatErrors("failed to load plugins", "", errs) http.Error(w, err.Error(), http.StatusInternalServerError) errs = []error{} } // determine if path is directory, file, or exists if fileInfo, err = os.Stat(path); err == nil { if fileInfo.IsDir() { // get the final archive path archivePath := fmt.Sprintf("%s.tar.gz", path) log.Debug(). Str("archive_path", archivePath). Str("type", "directory"). Msg("Service.Download()") 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, pluginArgs, errs) if len(errs) > 0 { log.Error(). Errs("errs", errs). Msg("errors occurred loading plugins") err = util.FormatErrors("failed to load plugins", "", errs) http.Error(w, err.Error(), http.StatusInternalServerError) errs = []error{} return } // 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 } // send the archive back as response w.Header().Add("FILETYPE", "archive") 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, pluginArgs, errs) if len(errs) > 0 { log.Error(). Errs("errs", errs). Msg("errors occurred loading plugins") err = util.FormatErrors("failed to load plugins", "", errs) http.Error(w, err.Error(), http.StatusInternalServerError) errs = []error{} return } 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.Init() if err != nil { log.Error(). Err(err). Str("plugin", hook.Plugin.Name()). Msg("failed to initialize plugin") continue } err = hook.Run() if err != nil { log.Error(). Err(err). Str("plugin", hook.Plugin.Name()). Msg("failed to run plugin") continue } err = hook.Cleanup() if err != nil { log.Error(). Err(err). Str("plugin", hook.Plugin.Name()). Msg("failed to cleanup 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 } // send processed (with plugins) file back as response w.Header().Add("FILETYPE", "file") w.Write(data.([]byte)) } else { // send non-processed file back as response w.Header().Add("FILETYPE", "file") 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) { var ( path = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/upload") body []byte dirpath string err error ) // show what we're uploading log.Debug(). Str("path", path). Msg("Service.Upload()") // take the provided path and store the file contents dirpath = filepath.Dir(path) err = os.MkdirAll(dirpath, 0o777) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // write file to disk body, err = io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } err = os.WriteFile(path, body, 0o777) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } } 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) Delete() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( path = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/delete") err error ) err = os.RemoveAll(path) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } } 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": "The makeshift server 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 { // check for special case profile ID (e.g. '*' or 'all') useAll := slices.ContainsFunc(profileIDs, func(profileID string) bool { return profileID == "*" || profileID == "all" }) if useAll { var ( dirpath = s.PathForProfiles() profiles []*makeshift.Profile err error ) profiles, err = LoadProfilesFromDir(dirpath) if err != nil { errs = append(errs, err) return errs } store.Set("profiles", profiles) return nil } // 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 > s.ProfilesMaxCount { 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 > s.PluginsMaxCount { log.Warn().Msg("max plugins count reached or exceeded...stopping") return hooks, errs } if pluginName == "" { log.Warn().Msgf("no plugin name found with index %d...skipping", i) 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 }