diff --git a/api/cdb/cached_client.go b/api/cdb/cached_client.go index b49a80de..fab88221 100644 --- a/api/cdb/cached_client.go +++ b/api/cdb/cached_client.go @@ -13,6 +13,8 @@ type CachedCDBClient struct { search_cache *cache.Cache[string, []*Package] dependency_cache *cache.Cache[string, PackageDependency] detail_cache *cache.Cache[string, *PackageDetails] + updates PackageUpdates + updates_time time.Time } func NewCachedClient(client *CDBClient, ttl time.Duration) *CachedCDBClient { @@ -69,3 +71,15 @@ func (c *CachedCDBClient) GetDetails(author, name string) (*PackageDetails, erro } return res, nil } + +func (c *CachedCDBClient) GetUpdates() (PackageUpdates, error) { + last_update := time.Since(c.updates_time) + if last_update > c.ttl || c.updates == nil { + var err error + c.updates, err = c.client.GetUpdates() + if err != nil { + return nil, err + } + } + return c.updates, nil +} diff --git a/api/cdb/client.go b/api/cdb/client.go index 164e3dc7..74bedb2d 100644 --- a/api/cdb/client.go +++ b/api/cdb/client.go @@ -109,6 +109,12 @@ func (c *CDBClient) GetRelease(author, name string, id int) (*PackageRelease, er return pr, err } +func (c *CDBClient) GetUpdates() (PackageUpdates, error) { + pu := PackageUpdates{} + err := c.get("api/updates", &pu, nil) + return pu, err +} + func (c *CDBClient) GetScreenshots(author, name string) ([]*PackageScreenshot, error) { ps := []*PackageScreenshot{} err := c.get(fmt.Sprintf("api/packages/%s/%s/screenshots", author, name), &ps, nil) diff --git a/api/cdb/types.go b/api/cdb/types.go index 033cb711..9cbabe3e 100644 --- a/api/cdb/types.go +++ b/api/cdb/types.go @@ -133,6 +133,8 @@ type DependencyInfo struct { type PackageDependency map[string][]*DependencyInfo +type PackageUpdates map[string]int + type MinetestVersion struct { IsDev bool `json:"is_dev"` Name string `json:"name"` diff --git a/db/migrations/08_mod_latest_version.up.sql b/db/migrations/08_mod_latest_version.up.sql new file mode 100644 index 00000000..0e2e5500 --- /dev/null +++ b/db/migrations/08_mod_latest_version.up.sql @@ -0,0 +1 @@ +alter table mod add column latest_version varchar(64) not null default ''; diff --git a/modmanager/manager.go b/modmanager/manager.go index eb9c2da6..4f2db731 100644 --- a/modmanager/manager.go +++ b/modmanager/manager.go @@ -36,11 +36,6 @@ func (m *ModManager) Create(mod *types.Mod) error { return handler.Create(m.handlercontext, mod) } -func (m *ModManager) Status(mod *types.Mod) (*ModStatus, error) { - handler := m.handlers[mod.SourceType] - return handler.Status(m.handlercontext, mod) -} - func (m *ModManager) Update(mod *types.Mod, version string) error { handler := m.handlers[mod.SourceType] return handler.Update(m.handlercontext, mod, version) diff --git a/modmanager/manager_cdb_test.go b/modmanager/manager_cdb_test.go index 0dd934f1..622f5768 100644 --- a/modmanager/manager_cdb_test.go +++ b/modmanager/manager_cdb_test.go @@ -17,7 +17,7 @@ func TestLatestCDBRelease(t *testing.T) { Name: "blockexchange", ModType: types.ModTypeMod, SourceType: types.SourceTypeCDB, - Author: "buckaroobanzay", + Author: "BuckarooBanzay", } assert.NoError(t, mm.Create(mod)) assert.True(t, mod.Version != "") @@ -27,13 +27,14 @@ func TestLatestCDBRelease(t *testing.T) { assert.NotNil(t, mods) assert.Equal(t, 1, len(mods)) - status, err := mm.Status(mod) + err = mm.CheckUpdates() assert.NoError(t, err) - assert.NotNil(t, status) - assert.Equal(t, mod.Version, status.CurrentVersion) - assert.Equal(t, mod.Version, status.LatestVersion) - err = mm.Update(mod, mod.Version) + mod, err = app.Repos.ModRepo.GetByID(mod.ID) + assert.NoError(t, err) + assert.Equal(t, mod.Version, mod.LatestVersion) + + err = mm.Update(mod, mod.LatestVersion) assert.NoError(t, err) err = mm.Remove(mod) diff --git a/modmanager/manager_git_test.go b/modmanager/manager_git_test.go index 53eb436a..01ad3b37 100644 --- a/modmanager/manager_git_test.go +++ b/modmanager/manager_git_test.go @@ -65,20 +65,15 @@ func TestCheckoutHash(t *testing.T) { assert.Equal(t, 1, len(mods)) // check remote status - status, err := mm.Status(mod) + err = mm.CheckUpdates() assert.NoError(t, err) - assert.NotNil(t, status) - assert.Equal(t, "fe34e3f3cd3e066ba0be76f9df46c11e66411496", status.CurrentVersion) - assert.True(t, status.LatestVersion != "") - assert.True(t, status.LatestVersion != status.CurrentVersion) - // update - assert.NoError(t, mm.Update(mod, status.LatestVersion)) - status2, err := mm.Status(mod) + mod, err = app.Repos.ModRepo.GetByID(mod.ID) assert.NoError(t, err) - assert.NotNil(t, status2) - assert.Equal(t, status.LatestVersion, status2.CurrentVersion) - assert.Equal(t, status.LatestVersion, status2.LatestVersion) + assert.NotEqual(t, "fe34e3f3cd3e066ba0be76f9df46c11e66411496", mod.LatestVersion) + + // update + assert.NoError(t, mm.Update(mod, mod.LatestVersion)) // remove assert.NoError(t, mm.Remove(mod)) diff --git a/modmanager/source_cdb.go b/modmanager/source_cdb.go index 516a85a6..9295112a 100644 --- a/modmanager/source_cdb.go +++ b/modmanager/source_cdb.go @@ -9,9 +9,11 @@ import ( "path" "strconv" "strings" + "time" ) var cli = cdb.New() +var cached_cli = cdb.NewCachedClient(cli, time.Hour) type ContentDBModHandler struct{} @@ -69,7 +71,6 @@ func (h *ContentDBModHandler) installMod(ctx *HandlerContext, mod *types.Mod, re default: return fmt.Errorf("mod type not supported: %s", mod.ModType) } - fmt.Printf("Fullpath: '%s' entry: '%s'\n", fullpath, f.Name) // create basedir if it does not exist basedir := path.Dir(fullpath) @@ -150,20 +151,6 @@ func (h *ContentDBModHandler) Create(ctx *HandlerContext, mod *types.Mod) error return ctx.Repo.Create(mod) } -func (h *ContentDBModHandler) Status(ctx *HandlerContext, mod *types.Mod) (*ModStatus, error) { - release, err := h.getLatestRelease(ctx, mod) - if err != nil { - return nil, fmt.Errorf("could not fetch latest release: %v", err) - } - - s := &ModStatus{ - CurrentVersion: mod.Version, - LatestVersion: fmt.Sprintf("%d", release.ID), - } - - return s, nil -} - func (h *ContentDBModHandler) Update(ctx *HandlerContext, mod *types.Mod, version string) error { release_id, err := strconv.Atoi(version) @@ -196,3 +183,18 @@ func (h *ContentDBModHandler) Remove(ctx *HandlerContext, mod *types.Mod) error return ctx.Repo.Delete(mod.ID) } + +func (h *ContentDBModHandler) CheckUpdate(ctx *HandlerContext, mod *types.Mod) (bool, error) { + updates, err := cached_cli.GetUpdates() + if err != nil { + return false, fmt.Errorf("could not get updates: %v", err) + } + + v := updates[fmt.Sprintf("%s/%s", mod.Author, mod.Name)] + if v > 0 { + mod.LatestVersion = fmt.Sprintf("%d", v) + return true, nil + } + + return false, nil +} diff --git a/modmanager/source_git.go b/modmanager/source_git.go index 1e48c658..247d5822 100644 --- a/modmanager/source_git.go +++ b/modmanager/source_git.go @@ -70,40 +70,6 @@ func (h *GitModHandler) Create(ctx *HandlerContext, mod *types.Mod) error { return ctx.Repo.Create(mod) } -func (h *GitModHandler) Status(ctx *HandlerContext, mod *types.Mod) (*ModStatus, error) { - status := &ModStatus{} - - dir := getDir(ctx.WorldDir, mod) - - r, err := git.PlainOpen(dir) - if err != nil { - return status, err - } - - heah, err := r.Head() - if err != nil { - return status, err - } - status.CurrentVersion = heah.Hash().String() - - rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{ - Name: "origin", - URLs: []string{mod.URL}, - }) - - refs, err := rem.List(&git.ListOptions{}) - if err != nil { - return status, err - } - for _, ref := range refs { - if ref.Name() == plumbing.ReferenceName(mod.Branch) { - status.LatestVersion = ref.Hash().String() - } - } - - return status, nil -} - func (h *GitModHandler) Update(ctx *HandlerContext, mod *types.Mod, version string) error { dir := getDir(ctx.WorldDir, mod) @@ -122,9 +88,16 @@ func (h *GitModHandler) Update(ctx *HandlerContext, mod *types.Mod, version stri return err } - return w.Checkout(&git.CheckoutOptions{ + err = w.Checkout(&git.CheckoutOptions{ Hash: plumbing.NewHash(version), }) + if err != nil { + return err + } + + mod.Version = version + return ctx.Repo.Update(mod) + } func (h *GitModHandler) Remove(ctx *HandlerContext, mod *types.Mod) error { @@ -137,3 +110,22 @@ func (h *GitModHandler) Remove(ctx *HandlerContext, mod *types.Mod) error { return ctx.Repo.Delete(mod.ID) } + +func (h *GitModHandler) CheckUpdate(ctx *HandlerContext, mod *types.Mod) (bool, error) { + rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{ + Name: "origin", + URLs: []string{mod.URL}, + }) + + refs, err := rem.List(&git.ListOptions{}) + if err != nil { + return false, fmt.Errorf("git error: %v", err) + } + for _, ref := range refs { + if ref.Name() == plumbing.ReferenceName(mod.Branch) { + mod.LatestVersion = ref.Hash().String() + } + } + + return mod.LatestVersion != mod.Version, nil +} diff --git a/modmanager/types.go b/modmanager/types.go index 6675e5e4..d5afe2f2 100644 --- a/modmanager/types.go +++ b/modmanager/types.go @@ -17,7 +17,7 @@ type HandlerContext struct { type SourceTypeHandler interface { Create(ctx *HandlerContext, mod *types.Mod) error - Status(ctx *HandlerContext, mod *types.Mod) (*ModStatus, error) Update(ctx *HandlerContext, mod *types.Mod, version string) error Remove(ctx *HandlerContext, mod *types.Mod) error + CheckUpdate(ctx *HandlerContext, mod *types.Mod) (bool, error) } diff --git a/modmanager/updates.go b/modmanager/updates.go new file mode 100644 index 00000000..18742fcc --- /dev/null +++ b/modmanager/updates.go @@ -0,0 +1,28 @@ +package modmanager + +import ( + "fmt" +) + +func (m *ModManager) CheckUpdates() error { + mods, err := m.repo.GetAll() + if err != nil { + return fmt.Errorf("get all mods failed: %v", err) + } + + for _, mod := range mods { + h := m.handlers[mod.SourceType] + updated, err := h.CheckUpdate(m.handlercontext, mod) + if err != nil { + return fmt.Errorf("update check failed for mod '%s': %v", mod.Name, err) + } + if updated { + err = m.repo.Update(mod) + if err != nil { + return fmt.Errorf("failed to update mod-data for '%s': %v", mod.Name, err) + } + } + } + + return nil +} diff --git a/public/js/api/mods.js b/public/js/api/mods.js index f966662b..460a831d 100644 --- a/public/js/api/mods.js +++ b/public/js/api/mods.js @@ -9,4 +9,12 @@ export const create_mod = mod => protected_fetch("api/mods", { body: JSON.stringify(mod) }); +export const update_mod = (mod, version) => protected_fetch(`api/mods/${mod.id}/update/${version}`, { + method: "POST" +}); + +export const check_updates = () => protected_fetch("api/mods/checkupdates", { + method: "POST" +}); + export const remove_mod = id => fetch(`api/mods/${id}`, {method: "DELETE"}); diff --git a/public/js/components/pages/mods/Mods.js b/public/js/components/pages/mods/Mods.js index b4f14e7f..a104d2a2 100644 --- a/public/js/components/pages/mods/Mods.js +++ b/public/js/components/pages/mods/Mods.js @@ -1,4 +1,4 @@ -import { add, remove, get_all, is_busy, get_git_mod } from '../../../service/mods.js'; +import { add, remove, get_all, is_busy, get_git_mod, update_mod, check_updates } from '../../../service/mods.js'; import FeedbackButton from '../../FeedbackButton.js'; import DefaultLayout from '../../layouts/DefaultLayout.js'; import CDBPackageLink from '../../CDBPackageLink.js'; @@ -45,9 +45,11 @@ export default { branch: "refs/heads/master" }); }, + update_mod: update_mod, remove: remove, get_mods: get_all, - get_git_mod: get_git_mod + get_git_mod: get_git_mod, + check_updates: check_updates }, computed: { busy: is_busy @@ -71,6 +73,15 @@ export default { +
+
+
+ +
+
@@ -79,6 +90,7 @@ export default { + @@ -135,7 +147,7 @@ export default { - + @@ -156,6 +168,9 @@ export default { + diff --git a/public/js/service/mods.js b/public/js/service/mods.js index 5453d2aa..c75d22c3 100644 --- a/public/js/service/mods.js +++ b/public/js/service/mods.js @@ -1,4 +1,4 @@ -import { list_mods, create_mod, remove_mod } from '../api/mods.js'; +import { list_mods, create_mod, remove_mod, update_mod as api_update_mod, check_updates as api_check_updates } from '../api/mods.js'; const store = Vue.reactive({ list: [], @@ -15,6 +15,12 @@ export const update = () => { .finally(() => store.busy = false); }; +export const update_mod = (m, v) => { + store.busy = true; + api_update_mod(m, v) + .then(() => update()); +}; + export const is_busy = () => store.busy; export const add = m => create_mod(m).then(update); @@ -27,4 +33,11 @@ export const get_cdb_mod = (author, name) => store.list.find(m => m.name == name export const get_git_mod = name => store.list.find(m => m.name == name); -export const get_game = () => store.list.find(m => m.mod_type == "game"); \ No newline at end of file +export const get_game = () => store.list.find(m => m.mod_type == "game"); + +export const check_updates = () => { + store.busy = true; + api_check_updates() + .then(() => update()); +}; + diff --git a/types/mod.go b/types/mod.go index d1fbf438..447fdc80 100644 --- a/types/mod.go +++ b/types/mod.go @@ -17,19 +17,20 @@ const ( ) type Mod struct { - ID string `json:"id"` - Name string `json:"name"` - Author string `json:"author"` - ModType ModType `json:"mod_type"` - SourceType SourceType `json:"source_type"` - URL string `json:"url"` - Branch string `json:"branch"` - Version string `json:"version"` - AutoUpdate bool `json:"auto_update"` + ID string `json:"id"` + Name string `json:"name"` + Author string `json:"author"` + ModType ModType `json:"mod_type"` + SourceType SourceType `json:"source_type"` + URL string `json:"url"` + Branch string `json:"branch"` + Version string `json:"version"` + LatestVersion string `json:"latest_version"` + AutoUpdate bool `json:"auto_update"` } func (m *Mod) Columns(action string) []string { - return []string{"id", "name", "author", "mod_type", "source_type", "url", "branch", "version", "auto_update"} + return []string{"id", "name", "author", "mod_type", "source_type", "url", "branch", "version", "latest_version", "auto_update"} } func (m *Mod) Table() string { @@ -37,9 +38,9 @@ func (m *Mod) Table() string { } func (m *Mod) Scan(action string, r func(dest ...any) error) error { - return r(&m.ID, &m.Name, &m.Author, &m.ModType, &m.SourceType, &m.URL, &m.Branch, &m.Version, &m.AutoUpdate) + return r(&m.ID, &m.Name, &m.Author, &m.ModType, &m.SourceType, &m.URL, &m.Branch, &m.Version, &m.LatestVersion, &m.AutoUpdate) } func (m *Mod) Values(action string) []any { - return []any{m.ID, m.Name, m.Author, m.ModType, m.SourceType, m.URL, m.Branch, m.Version, m.AutoUpdate} + return []any{m.ID, m.Name, m.Author, m.ModType, m.SourceType, m.URL, m.Branch, m.Version, m.LatestVersion, m.AutoUpdate} } diff --git a/web/modmanager.go b/web/modmanager.go index fc984c3d..40c06aa6 100644 --- a/web/modmanager.go +++ b/web/modmanager.go @@ -73,17 +73,9 @@ func (a *Api) DeleteMod(w http.ResponseWriter, r *http.Request, claims *types.Cl } } -func (a *Api) ModStatus(w http.ResponseWriter, r *http.Request, claims *types.Claims) { - vars := mux.Vars(r) - id := vars["id"] - - m, err := a.app.ModManager.Mod(id) - if err != nil { - SendError(w, 500, err.Error()) - return - } - status, err := a.app.ModManager.Status(m) - Send(w, status, err) +func (a *Api) ModsCheckUpdates(w http.ResponseWriter, r *http.Request, claims *types.Claims) { + err := a.app.ModManager.CheckUpdates() + Send(w, true, err) } func (a *Api) ModsValidate(w http.ResponseWriter, r *http.Request, claims *types.Claims) { diff --git a/web/setup.go b/web/setup.go index d3c0cb14..45d4664b 100644 --- a/web/setup.go +++ b/web/setup.go @@ -144,9 +144,9 @@ func Setup(a *app.App) error { modsapi.HandleFunc("", api.Secure(api.GetMods)).Methods(http.MethodGet) modsapi.HandleFunc("", api.Secure(api.CreateMod)).Methods(http.MethodPost) modsapi.HandleFunc("/validate", api.Secure(api.ModsValidate)).Methods(http.MethodGet) + modsapi.HandleFunc("/checkupdates", api.Secure(api.ModsCheckUpdates)).Methods(http.MethodPost) modsapi.HandleFunc("/{id}/update/{version}", api.Secure(api.UpdateModVersion)).Methods(http.MethodPost) modsapi.HandleFunc("/{id}", api.Secure(api.DeleteMod)).Methods(http.MethodDelete) - modsapi.HandleFunc("/{id}/status", api.Secure(api.ModStatus)).Methods(http.MethodGet) cdbapi := apir.PathPrefix("/cdb").Subrouter() cdbapi.Use(SecureHandler(api.FeatureCheck(types.FEATURE_MODMANAGEMENT), api.PrivCheck("server")))
Source-Type Source VersionLatest Version Auto-update Actions
{{mod.mod_type}} {{mod.version}} + {{mod.latest_version}} + @@ -168,14 +183,14 @@ export default {