From 526b98ff5eb00370cfc99e602bc393609063a5da Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Fri, 18 Jan 2019 12:26:32 -0700 Subject: [PATCH 1/6] Add commands for removing a mod from a pack There are 2 commands: single and recursive. The single version only removes the 1 mod that was specified, ignoring dependencies. The recursive version will remove the requested mod and all dependents and dependencies. --- algo/topo.go | 104 ++++++++++++++++ db.go | 102 ++++++++++++++-- main.go | 328 +++++++++++++++++++++++++++++++++++++++++++++++---- mod.go | 14 +++ modpack.go | 13 +- util.go | 13 ++ 6 files changed, 538 insertions(+), 36 deletions(-) create mode 100644 algo/topo.go diff --git a/algo/topo.go b/algo/topo.go new file mode 100644 index 0000000..a52c382 --- /dev/null +++ b/algo/topo.go @@ -0,0 +1,104 @@ +package algo + +import "fmt" + +type Value interface{} + +type Graph map[Value]*Node + +type Node struct { + Value Value + graph Graph + Dependents, + Dependencies, + Optionals map[*Node]struct{} +} + + +func (n *Node) String() string { + return fmt.Sprintf("%+v", n.Value) +} + +func (n *Node) AddDependencies(keys ...Value) { + for _, key := range keys { + dep := n.graph.AddNode(key) + n.Dependencies[dep] = struct{}{} + dep.Dependents[n] = struct{}{} + } +} + +func (n *Node) AddOptionals(keys ...Value) { + for _, key := range keys { + dep := n.graph.AddNode(key) + n.Optionals[dep] = struct{}{} + } +} + +func (n *Node) IsRoot() bool { + return len(n.Dependents) == 0 +} + +func (n *Node) IsLeaf() bool { + return len(n.Dependencies) == 0 +} + + +func MakeGraph() Graph { + return make(Graph) +} + +func (g Graph) AddNode(key Value) *Node { + if g[key] == nil { + g[key] = &Node{ + Value: key, + graph: g, + Dependents: make(map[*Node]struct{}), + Dependencies: make(map[*Node]struct{}), + Optionals: make(map[*Node]struct{}), + } + } + return g[key] +} + +func (g Graph) RemoveNode(key Value) { + if g[key] == nil { + return + } + n := g[key] + for _, d := range g { + delete(d.Dependencies, n) + delete(d.Dependents, n) + delete(d.Optionals, n) + } + delete(g, key) +} + +func (g Graph) Sorted() []*Node { + sorted := make([]*Node, 0, len(g)) + degree := make(map[*Node]int) + + var next []*Node + for _, n := range g { + if n.IsRoot() { + next = append(next, n) + } else { + degree[n] = len(n.Dependents) + } + } + + for len(next) > 0 { + n := next[0] + next = next[1:] + + sorted = append(sorted, n) + + for d := range n.Dependencies { + degree[d]-- + if degree[d] == 0 { + next = append(next, d) + } + } + } + + return sorted +} diff --git a/db.go b/db.go index 4ed3d7e..40c180b 100644 --- a/db.go +++ b/db.go @@ -21,15 +21,19 @@ import ( "compress/bzip2" "database/sql" "fmt" + "log" "os" "path/filepath" - "regexp" + "strconv" + "strings" "golang.org/x/text/language" "golang.org/x/text/message" _ "github.com/mattn/go-sqlite3" + + "mcdex/algo" ) type Database struct { @@ -38,6 +42,14 @@ type Database struct { version string } +type DepType int + +const ( + Required = 1 + Optional = 2 + Embedded = 3 +) + func OpenDatabase() (*Database, error) { db := new(Database) @@ -231,8 +243,8 @@ func (db *Database) getLatestFileTstamp() (int, error) { func (db *Database) getLatestModFile(modID int, mcvsn string) (*ModFile, error) { // First, look up the modid for the given name - var name, desc string - err := db.sqlDb.QueryRow("select name, description from projects where type = 0 and projectid = ?", modID).Scan(&name, &desc) + var name, slug, desc string + err := db.sqlDb.QueryRow("select name, slug, description from projects where type = 0 and projectid = ?", modID).Scan(&name, &slug, &desc) switch { case err == sql.ErrNoRows: return nil, fmt.Errorf("No mod found %d", modID) @@ -251,7 +263,7 @@ func (db *Database) getLatestModFile(modID int, mcvsn string) (*ModFile, error) return nil, err } - return &ModFile{fileID: fileID, modID: modID, modName: name, modDesc: desc}, nil + return &ModFile{fileID: fileID, modID: modID, modName: name, slug: slug, modDesc: desc}, nil } func (db *Database) findProjectBySlug(slug string, ptype int) (int, error) { @@ -285,7 +297,7 @@ func (db *Database) findModByName(name string) (int, error) { func (db *Database) findModFile(modID, fileID int, mcversion string) (*ModFile, error) { // Try to match the file ID if fileID > 0 { - err := db.sqlDb.QueryRow("select fileid from files where projectid = ? and fileid = ? and version = ?", modID, fileID, mcversion).Scan(&fileID) + err := db.sqlDb.QueryRow("select projectid from files where fileid = ? and version = ?", fileID, mcversion).Scan(&modID) if err != nil { return nil, fmt.Errorf("No matching file ID for %s version", mcversion) } @@ -298,13 +310,13 @@ func (db *Database) findModFile(modID, fileID int, mcversion string) (*ModFile, } // We matched some file; pull the name and description for the mod - var name, desc string - err := db.sqlDb.QueryRow("select slug, description from projects where projectid = ?", modID).Scan(&name, &desc) + var name, slug, desc string + err := db.sqlDb.QueryRow("select name, slug, description from projects where projectid = ?", modID).Scan(&name, &slug, &desc) if err != nil { return nil, fmt.Errorf("Failed to retrieve name, description for mod %d: %+v", modID, err) } - return &ModFile{fileID: fileID, modID: modID, modName: name, modDesc: desc}, nil + return &ModFile{fileID: fileID, modID: modID, modName: name, slug: slug, modDesc: desc}, nil } func (db *Database) getDeps(fileID int) ([]int, error) { @@ -351,6 +363,76 @@ func (db *Database) getLatestPackURL(slug string) (string, error) { } // Construct a URL using the slug and file ID - return fmt.Sprintf("https://minecraft.curseforge.com/projects/%d/files/%d/download", pid, fileID), nil; + return fmt.Sprintf("https://minecraft.curseforge.com/projects/%d/files/%d/download", pid, fileID), nil +} + +func (db *Database) buildDepGraph(m *ModPack) (algo.Graph, error) { + + var fileIds map[int]*ManifestFileEntry + var fileIdsString strings.Builder + g := algo.MakeGraph() -} \ No newline at end of file + // Load all files from manifest + { + nameQuery, err := db.sqlDb.Prepare("SELECT DISTINCT name FROM files f, projects p WHERE f.fileid = ? AND f.projectid = ? AND p.projectid = f.projectid") + if err != nil { + return nil, err + } + defer nameQuery.Close() + + files, _ := m.manifest.S("files").Children() + fileIds = make(map[int]*ManifestFileEntry, len(files)) + for i, file := range files { + record := ManifestFileEntry{idx: i} + if file.Exists("filename") { + record.file = file.S("filename").Data().(string) + } + + record.projId = int(file.S("projectID").Data().(float64)) + record.fileId = int(file.S("fileID").Data().(float64)) + err = nameQuery.QueryRow(record.fileId, record.projId).Scan(&record.name) + switch { + case err == sql.ErrNoRows: + log.Printf("No mod found in database with project id %d and file id %d - File: %q\n\tDependency resolution may be incomplete!", record.projId, record.fileId, record.file) + case err != nil: + return nil, err + } + + fileIds[record.fileId] = &record + g.AddNode(&record) + + fileIdsString.WriteString(strconv.Itoa(record.fileId)) + fileIdsString.WriteByte(',') + } + // Simple hack to deal with trailing comma or empty + fileIdsString.WriteByte('0') + } + + // Load dependencies and add to graph + { + rows, err := db.sqlDb.Query(fmt.Sprintf("SELECT DISTINCT d.fileid, f.fileid, level FROM deps d, files f WHERE level <> 3 AND f.projectid = d.projectid AND d.fileid IN (%[1]s) AND f.fileid IN (%[1]s)", fileIdsString.String())) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var fileid, depid int + var depType DepType + if err = rows.Scan(&fileid, &depid, &depType); err != nil { + return nil, err + } + + node := g[fileIds[fileid]] + dep := fileIds[depid] + switch depType { + case Required: + node.AddDependencies(dep) + case Optional: + node.AddOptionals(dep) + } + } + } + + return g, nil +} diff --git a/main.go b/main.go index 4802cca..eb3fdd3 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,8 @@ import ( "time" "github.com/xeonx/timeago" + + "mcdex/algo" ) var version string @@ -91,7 +93,6 @@ var gCommands = map[string]command{ ArgsCount: 0, Args: "[]", }, - "mod.select": { Fn: cmdModSelect, Desc: "Select a mod to include in the specified pack", @@ -104,6 +105,18 @@ var gCommands = map[string]command{ ArgsCount: 2, Args: " []", }, + "mod.remove.single": { + Fn: cmdModRemoveSingle, + Desc: "Removes a single mod from the specified pack", + ArgsCount: 2, + Args: " ", + }, + "mod.remove.recursive": { + Fn: cmdModRemoveRecursive, + Desc: "Removes a mod, and all dependant mods, from the specified pack", + ArgsCount: 2, + Args: " ", + }, "mod.update.all": { Fn: cmdModUpdateAll, Desc: "Update all mods entries to latest available file", @@ -293,6 +306,261 @@ func cmdModSelectClient() error { return _modSelect(flag.Arg(1), flag.Arg(2), flag.Arg(3), true) } + +func _initRemove(cp *ModPack, modName string, maxDepth int) (info *DepInfo, err error) { + db, err := OpenDatabase() + if err != nil { + return + } + + // Try to lookup the mod ID by name + modID, err := db.findModByName(modName) + if err != nil { + return + } + + modID, err = cp.lookupFileId(modID) + if err != nil { + err = fmt.Errorf("failed to lookup id of mod: %v", err) + return + } + + return _processDeps(cp, db, modID, maxDepth) +} + +type DepInfo struct { + target *ManifestFileEntry + dependents map[*ManifestFileEntry][]*ManifestFileEntry + dependencies map[*ManifestFileEntry][]*ManifestFileEntry + optionals map[*ManifestFileEntry][]*ManifestFileEntry +} +func _processDeps(cp *ModPack, db *Database, targetModId, maxDepth int) (*DepInfo, error) { + mods, err := db.buildDepGraph(cp) + if err != nil { + return nil, err + } + + relatedMods := make(map[*algo.Node]struct{}) + result := DepInfo{ + dependents: make(map[*ManifestFileEntry][]*ManifestFileEntry), + dependencies: make(map[*ManifestFileEntry][]*ManifestFileEntry), + optionals: make(map[*ManifestFileEntry][]*ManifestFileEntry), + } + + // Find all dependents + { + var deps []*algo.Node + for _, m := range mods { + entry := m.Value.(*ManifestFileEntry) + if entry.fileId == targetModId { + deps = append(deps, m) + result.target = entry + break + } + } + + depths := []int{0} + for len(deps) > 0 && (maxDepth < 0 || depths[0] < maxDepth) { + d := deps[0] + depth := depths[0] + 1 + relatedMods[d] = struct{}{} + deps = deps[1:] + depths = depths[1:] + + for dd := range d.Dependents { + depths = append(depths, depth) + deps = append(deps, dd) + r := result.dependents[dd.Value.(*ManifestFileEntry)] + result.dependents[dd.Value.(*ManifestFileEntry)] = append(r, d.Value.(*ManifestFileEntry)) + } + } + } + + // Find all dependencies that will no longer have a dependent + { + sorted := mods.Sorted() + Outer: + for _, m := range sorted { + // Only looking at dependencies here, any roots would have been included above + if m.IsRoot() { + continue + } + // Already included + if _, found := relatedMods[m]; found { + continue + } + var parents []*ManifestFileEntry + for d := range m.Dependents { + if _, found := relatedMods[d]; !found { + continue Outer // Still has a dependent + } + parents = append(parents, d.Value.(*ManifestFileEntry)) + } + + // All dependents included + relatedMods[m] = struct{}{} + result.dependencies[m.Value.(*ManifestFileEntry)] = parents + } + } + + // Find all related optional dependencies + { + for _, m := range mods { + if _, found := relatedMods[m]; found { + continue + } + var opts []*ManifestFileEntry + for o := range m.Optionals { + if _, found := relatedMods[o]; found { + opts = append(opts, o.Value.(*ManifestFileEntry)) + } + } + if len(opts) > 0 { + result.optionals[m.Value.(*ManifestFileEntry)] = opts + } + } + } + + return &result, nil +} + +func cmdModRemoveSingle() error { + // Try to open the mod pack + cp, err := NewModPack(flag.Arg(1), true, ARG_MMC) + if err != nil { + return err + } + + depInfo, err := _initRemove(cp, flag.Arg(2), 1) + if err != nil { + return err + } + + fmt.Println() + fmt.Println("Preparing to remove the mod:") + fmt.Printf("\t[%7d] %q (%s)\n", depInfo.target.fileId, depInfo.target.name, depInfo.target.file) + + fmt.Println() + fmt.Println("The following mods depend on the mod being removed and will no longer work:") + for d := range depInfo.dependents { + fmt.Printf("\t[%7d] %q (%s)\n", d.fileId, d.name, d.file) + } + + fmt.Println() + fmt.Println("The following mods were added as a dependency for the mod being removed and are no longer required:") + for d, l := range depInfo.dependencies { + if len(l) == 1 && l[0] == depInfo.target { + fmt.Printf("\t[%7d] %q (%s)\n", d.fileId, d.name, d.file) + } + } + + fmt.Println() + fmt.Println("The following mods optionally depend on the mod being removed:") + for o, deps := range depInfo.optionals { + for _, d := range deps { + if d == depInfo.target { + fmt.Printf("\t[%7d] %q (%s)\n", o.fileId, o.name, o.file) + break + } + } + } + + if !ARG_DRY_RUN { + fmt.Println() + fmt.Printf("Removing [%7d] %q - %q\n", depInfo.target.fileId, depInfo.target.name, depInfo.target.file) + if err := cp.manifest.ArrayRemove(depInfo.target.idx, "files"); err != nil { + return fmt.Errorf("failed to remove mod %q from manifest", depInfo.target.name) + } + if err := cp.saveManifest(); err != nil { + return fmt.Errorf("failed to save changes to manifest") + } + if depInfo.target.file != "" { + if err := os.Remove(filepath.Join(cp.modPath(), depInfo.target.file)); err != nil { + log.Println("Warning: ", err) + } + } + } + + return nil +} + +func cmdModRemoveRecursive() error { + // Try to open the mod pack + cp, err := NewModPack(flag.Arg(1), true, ARG_MMC) + if err != nil { + return err + } + + depInfo, err := _initRemove(cp, flag.Arg(2), -1) + if err != nil { + return err + } + + fmt.Println() + fmt.Println("Preparing to remove the mod:") + fmt.Printf("\t[%7d] %q (%s)\n", depInfo.target.fileId, depInfo.target.name, depInfo.target.file) + + rmList := []*ManifestFileEntry{depInfo.target} + + fmt.Println() + fmt.Println("The following dependent mods will also be removed:") + for m, d := range depInfo.dependents { + fmt.Printf("\t[%7d] %q (%s)\n\t\tDepends on %s\n", m.fileId, m.name, m.file, QuoteJoin(d,", ")) + rmList = append(rmList, m) + } + + fmt.Println() + fmt.Println("The following dependencies will also be removed:") + for m, d := range depInfo.dependencies { + fmt.Printf("\t[%7d] %q (%s)\n\t\tRequired by %s\n", m.fileId, m.name, m.file, QuoteJoin(d, ", ")) + rmList = append(rmList, m) + } + + fmt.Println() + fmt.Println("The following mods optionally depend on a mod being removed:") + for d, o := range depInfo.optionals { + fmt.Printf("\t[%7d] %q optionally depends on %s\n", d.fileId, d.name, QuoteJoin(o, ", ")) + rmList = append(rmList, d) + } + + if !ARG_DRY_RUN { + // Reverse sort by index so we remove from the back + sort.Slice(rmList, func(i, j int) bool { + return rmList[j].idx < rmList[i].idx + }) + + fmt.Println() + + var done int + for _, m := range rmList{ + fmt.Printf("Removing [%7d] %q - %q\n", m.fileId, m.name, m.file) + if err := cp.manifest.ArrayRemove(m.idx, "files"); err != nil { + log.Printf("Failed to remove mod %q from manifest\n", depInfo.target.name) + continue + } + done++ + if m.file != "" { + if err := os.Remove(filepath.Join(cp.modPath(), m.file)); err != nil { + log.Println("Warning: ", err) + } + } + } + if done > 0 { + if err := cp.saveManifest(); err != nil { + return fmt.Errorf("failed to save changes to manifest") + } + } else { + return fmt.Errorf("failed to remove any mods; no changes have been made") + } + if done < len(rmList) { + return fmt.Errorf("some mods could not be removed; pack may be in an invalid state") + } + } + + return nil +} + + var curseForgeRegex = regexp.MustCompile("/projects/([\\w-]*)(/files/(\\d+))?") func _modSelect(dir, mod, tag string, clientOnly bool) error { @@ -342,7 +610,7 @@ func _modSelect(dir, mod, tag string, clientOnly bool) error { } err = _selectModFromID(cp, db, modID, fileID, clientOnly) - if err == nil { + if err == nil && !ARG_DRY_RUN { return cp.saveManifest() } @@ -350,13 +618,38 @@ func _modSelect(dir, mod, tag string, clientOnly bool) error { } func _selectModFromID(pack *ModPack, db *Database, modID, fileID int, clientOnly bool) error { + if modFile, err := _lookupModByID(pack, db, modID, fileID); err == nil { + err := pack.selectModFile(modFile, clientOnly) + if err != nil { + return err + } + + deps, err := db.getDeps(modFile.fileID) + if err != nil { + return fmt.Errorf("Error pulling deps for %d: %+v", modFile.fileID, err) + } + + for _, dep := range deps { + err = _selectModFromID(pack, db, dep, 0, clientOnly) + if err != nil { + return err + } + } + + return nil + } else { + return err + } +} + +func _lookupModByID(pack *ModPack, db *Database, modID, fileID int) (*ModFile, error) { // At this point, we should have a modID and we may have a fileID. We want to walk major.minor.[patch] // versions, and find either the latest file for our version of minecraft or verify that the fileID // we have will work on this version major, minor, patch, err := parseVersion(pack.minecraftVersion()) if err != nil { // Invalid version string?! - return err + return nil, err } // Walk down patch versions, looking for our mod + file (or latest file if no fileID available) @@ -368,31 +661,12 @@ func _selectModFromID(pack *ModPack, db *Database, modID, fileID int, clientOnly vsn = fmt.Sprintf("%d.%d", major, minor) } - modFile, err := db.findModFile(modID, fileID, vsn) - if err == nil { - err := pack.selectModFile(modFile, clientOnly) - if err != nil { - return err - } - - deps, err := db.getDeps(modFile.fileID) - if err != nil { - return fmt.Errorf("Error pulling deps for %d: %+v", modFile.fileID, err) - } - - for _, dep := range deps { - err = _selectModFromID(pack, db, dep, 0, clientOnly) - if err != nil { - return err - } - } - - return nil + if modFile, err := db.findModFile(modID, fileID, vsn); err == nil { + return modFile, nil } } - // Didn't find a file that matches :( - return fmt.Errorf("No compatible file found for %d\n", modID) + return nil, fmt.Errorf("No compatible file found for %d\n", modID) } func listProjects(ptype int) error { @@ -687,6 +961,10 @@ func main() { os.Exit(-1) } + if ARG_DRY_RUN { + fmt.Printf("--- DRY RUN ---\n") + } + err = command.Fn() if err != nil { log.Fatalf("%+v\n", err) diff --git a/mod.go b/mod.go index 8e1562d..a245e49 100644 --- a/mod.go +++ b/mod.go @@ -22,4 +22,18 @@ type ModFile struct { modID int modName string modDesc string + slug string } + +type ManifestFileEntry struct { + idx int + file string + + fileId int + projId int + name string +} + +func (m *ManifestFileEntry) String() string { + return m.name +} \ No newline at end of file diff --git a/modpack.go b/modpack.go index 14fffbf..5fd38eb 100644 --- a/modpack.go +++ b/modpack.go @@ -362,7 +362,7 @@ func (pack *ModPack) selectModFile(modFile *ModFile, clientOnly bool) error { modInfo["projectID"] = modFile.modID modInfo["fileID"] = modFile.fileID modInfo["required"] = true - modInfo["desc"] = modFile.modName + modInfo["desc"] = modFile.slug if clientOnly { modInfo["clientOnly"] = true @@ -594,3 +594,14 @@ func (pack *ModPack) installServer() error { func (pack *ModPack) generateMMCConfig() error { return generateMMCConfig(pack) } + +func (pack *ModPack) lookupFileId(projectId int) (int, error) { + files, _ := pack.manifest.S("files").Children() + for _, file := range files { + if projectId == int(file.S("projectID").Data().(float64)) { + return int(file.S("fileID").Data().(float64)), nil + } + } + + return -1, fmt.Errorf("project %d not found in manifest", projectId) +} \ No newline at end of file diff --git a/util.go b/util.go index bac84d7..5f0cfca 100644 --- a/util.go +++ b/util.go @@ -27,6 +27,7 @@ import ( "net" "net/http" "os" + "reflect" "strconv" "strings" "time" @@ -260,3 +261,15 @@ func intValue(c *gabs.Container, path string) (int, error) { return 0, fmt.Errorf("Invalid type for %s: %+v", path, data) } } + +func QuoteJoin(slice interface{}, sep string) string { + s := reflect.ValueOf(slice) + if s.Kind() != reflect.Slice { + panic(&reflect.ValueError{Method: "QuoteJoin", Kind: s.Kind()}) + } + str := make([]string, s.Len()) + for i := 0; i < len(str); i++ { + str[i] = fmt.Sprintf("%q", s.Index(i)) + } + return strings.Join(str, sep) +} \ No newline at end of file From 7c7666c8cb7461f677d0278efe4ac11f98b51294 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Mon, 28 Jan 2019 20:50:09 -0700 Subject: [PATCH 2/6] Add pack.show command for showing mods included in an installed pack --- main.go | 28 ++++++++++++++++++++++++++++ mod.go | 14 +++++++++++++- modpack.go | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index eb3fdd3..4d94e0c 100644 --- a/main.go +++ b/main.go @@ -76,6 +76,12 @@ var gCommands = map[string]command{ ArgsCount: 1, Args: " []", }, + "pack.show": { + Fn: cmdPackShow, + Desc: "List all mods included in the specified installed pack", + ArgsCount: 1, + Args: "", + }, "info": { Fn: cmdInfo, Desc: "Show runtime info", @@ -275,6 +281,28 @@ func cmdPackInstall() error { return nil } +func cmdPackShow() error { + // Try to open the mod pack + cp, err := NewModPack(flag.Arg(1), true, ARG_MMC) + if err != nil { + return err + } + + db, err := OpenDatabase() + if err != nil { + return err + } + + mods, err := cp.getSelected(db) + sort.Slice(mods, func(i, j int) bool {return mods[i].name < mods[j].name}) + fmt.Println( " File ID || Project ID|| Name || Slug || Description || Released || Filename") + for _, mod := range mods { + console("%9d || %9d || %s || %s || %s || %v || %s\n", mod.fileID, mod.projectID, mod.name, mod.slug, mod.description, mod.timestamp, mod.filename) + } + + return nil +} + func cmdInfo() error { // Try to retrieve the latest available version info publishedVsn, err := readStringFromUrl("http://files.mcdex.net/release/latest") diff --git a/mod.go b/mod.go index a245e49..5067269 100644 --- a/mod.go +++ b/mod.go @@ -17,6 +17,8 @@ package main +import "time" + type ModFile struct { fileID int modID int @@ -36,4 +38,14 @@ type ManifestFileEntry struct { func (m *ManifestFileEntry) String() string { return m.name -} \ No newline at end of file +} + +type ModDetails struct { + fileID int + projectID int + name string + slug string + description string + filename string + timestamp time.Time +} diff --git a/modpack.go b/modpack.go index 5fd38eb..f7a99a9 100644 --- a/modpack.go +++ b/modpack.go @@ -19,12 +19,14 @@ package main import ( "archive/zip" + "database/sql" "fmt" + "io/ioutil" + "log" "os" "path/filepath" "strings" - - "io/ioutil" + "time" "github.com/Jeffail/gabs" ) @@ -604,4 +606,37 @@ func (pack *ModPack) lookupFileId(projectId int) (int, error) { } return -1, fmt.Errorf("project %d not found in manifest", projectId) +} + +func (pack *ModPack) getSelected(db *Database) ([]*ModDetails, error) { + detailQuery, err := db.sqlDb.Prepare("SELECT DISTINCT name, slug, description, f.tstamp FROM files f, projects p WHERE f.fileid = ? AND f.projectid = ? AND p.projectid = f.projectid") + if err != nil { + return nil, err + } + defer detailQuery.Close() + + files, _ := pack.manifest.S("files").Children() + results := make([]*ModDetails, 0, len(files)) + for _, file := range files { + var record ModDetails + if file.Exists("filename") { + record.filename = file.S("filename").Data().(string) + } + + record.projectID = int(file.S("projectID").Data().(float64)) + record.fileID = int(file.S("fileID").Data().(float64)) + var ts int64 + err = detailQuery.QueryRow(record.fileID, record.projectID).Scan(&record.name, &record.slug, &record.description, &ts) + switch { + case err == sql.ErrNoRows: + log.Printf("No mod found in database with project id %d and file id %d - File: %q", record.projectID, record.fileID, record.filename) + case err != nil: + return nil, err + } + record.timestamp = time.Unix(ts, 0) + + results = append(results, &record) + } + + return results, nil } \ No newline at end of file From 37611ee2b50d58409b848bcc0da88e95fe378c09 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Tue, 29 Jan 2019 20:37:36 -0700 Subject: [PATCH 3/6] Support removing multiple mods in a single command Also, don't remove dependencies that are optionally required by any remaining mods --- main.go | 239 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 135 insertions(+), 104 deletions(-) diff --git a/main.go b/main.go index 4d94e0c..9f0ee98 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ import ( "flag" "fmt" "log" + "math" "net/url" "os" "os/exec" @@ -113,15 +114,15 @@ var gCommands = map[string]command{ }, "mod.remove.single": { Fn: cmdModRemoveSingle, - Desc: "Removes a single mod from the specified pack", + Desc: "Remove individual mods from the specified pack, without handling dependencies", ArgsCount: 2, - Args: " ", + Args: " [mod names...]", }, "mod.remove.recursive": { Fn: cmdModRemoveRecursive, - Desc: "Removes a mod, and all dependant mods, from the specified pack", + Desc: "Remove specified mods, and all their dependant mods, from the specified pack", ArgsCount: 2, - Args: " ", + Args: " [mod names...]", }, "mod.update.all": { Fn: cmdModUpdateAll, @@ -335,41 +336,57 @@ func cmdModSelectClient() error { } -func _initRemove(cp *ModPack, modName string, maxDepth int) (info *DepInfo, err error) { +func _initRemove(cp *ModPack, modNames []string, maxDepth int) (info *DepInfo, err error) { db, err := OpenDatabase() if err != nil { return } - // Try to lookup the mod ID by name - modID, err := db.findModByName(modName) - if err != nil { - return + // Try to lookup the mod IDs by name + modIDs := make(map[int]struct{}, len(modNames)) + for _, modName := range modNames { + modID, err := db.findModByName(modName) + if err != nil { + log.Println(err) + continue + } + + modID, err = cp.lookupFileId(modID) + if err != nil { + log.Println(err) + continue + } + + modIDs[modID] = struct{}{} } - modID, err = cp.lookupFileId(modID) - if err != nil { - err = fmt.Errorf("failed to lookup id of mod: %v", err) + if len(modIDs) == 0 { + err = fmt.Errorf("no mods found") return } - return _processDeps(cp, db, modID, maxDepth) + return _processDeps(cp, db, modIDs, maxDepth) } type DepInfo struct { - target *ManifestFileEntry + targets map[*ManifestFileEntry]struct{} dependents map[*ManifestFileEntry][]*ManifestFileEntry dependencies map[*ManifestFileEntry][]*ManifestFileEntry optionals map[*ManifestFileEntry][]*ManifestFileEntry } -func _processDeps(cp *ModPack, db *Database, targetModId, maxDepth int) (*DepInfo, error) { +func _processDeps(cp *ModPack, db *Database, targetModIds map[int]struct{}, maxDepth int) (*DepInfo, error) { mods, err := db.buildDepGraph(cp) if err != nil { return nil, err } - relatedMods := make(map[*algo.Node]struct{}) + if maxDepth < 0 { + maxDepth = math.MaxInt32 + } + + relatedMods := make(map[*algo.Node]int) result := DepInfo{ + targets: make(map[*ManifestFileEntry]struct{}, len(targetModIds)), dependents: make(map[*ManifestFileEntry][]*ManifestFileEntry), dependencies: make(map[*ManifestFileEntry][]*ManifestFileEntry), optionals: make(map[*ManifestFileEntry][]*ManifestFileEntry), @@ -377,26 +394,36 @@ func _processDeps(cp *ModPack, db *Database, targetModId, maxDepth int) (*DepInf // Find all dependents { - var deps []*algo.Node + deps := make([]*algo.Node, 0, len(targetModIds)) + depths := make([]int, 0, len(targetModIds)) for _, m := range mods { entry := m.Value.(*ManifestFileEntry) - if entry.fileId == targetModId { - deps = append(deps, m) - result.target = entry - break + if _, found := relatedMods[m]; found { + continue + } + if _, found := targetModIds[entry.fileId]; !found { + continue } + + deps = append(deps, m) + depths = append(depths, 0) + result.targets[entry] = struct{}{} } - depths := []int{0} - for len(deps) > 0 && (maxDepth < 0 || depths[0] < maxDepth) { + for len(deps) > 0 && depths[0] < maxDepth { d := deps[0] - depth := depths[0] + 1 - relatedMods[d] = struct{}{} + depth := depths[0] deps = deps[1:] depths = depths[1:] + // Already included + if _, found := relatedMods[d]; found { + continue + } + + relatedMods[d] = depth for dd := range d.Dependents { - depths = append(depths, depth) + depths = append(depths, depth+1) deps = append(deps, dd) r := result.dependents[dd.Value.(*ManifestFileEntry)] result.dependents[dd.Value.(*ManifestFileEntry)] = append(r, d.Value.(*ManifestFileEntry)) @@ -418,15 +445,20 @@ func _processDeps(cp *ModPack, db *Database, targetModId, maxDepth int) (*DepInf continue } var parents []*ManifestFileEntry + minDepth := 0 for d := range m.Dependents { - if _, found := relatedMods[d]; !found { + depth, found := relatedMods[d] + if !found || depth >= maxDepth { continue Outer // Still has a dependent } + if (depth + 1) < minDepth { + minDepth = depth + 1 + } parents = append(parents, d.Value.(*ManifestFileEntry)) } // All dependents included - relatedMods[m] = struct{}{} + relatedMods[m] = minDepth result.dependencies[m.Value.(*ManifestFileEntry)] = parents } } @@ -439,8 +471,17 @@ func _processDeps(cp *ModPack, db *Database, targetModId, maxDepth int) (*DepInf } var opts []*ManifestFileEntry for o := range m.Optionals { - if _, found := relatedMods[o]; found { - opts = append(opts, o.Value.(*ManifestFileEntry)) + if depth, found := relatedMods[o]; !found || depth >= maxDepth { + continue + } + + entry := o.Value.(*ManifestFileEntry) + if _, found := result.dependencies[entry]; !found { + opts = append(opts, entry) + } else { + // Remove dependency that has an optional dependent + delete(relatedMods, o) + delete(result.dependencies, entry) } } if len(opts) > 0 { @@ -452,6 +493,46 @@ func _processDeps(cp *ModPack, db *Database, targetModId, maxDepth int) (*DepInf return &result, nil } +func _removeMods(cp *ModPack, mods []*ManifestFileEntry) error { + if ARG_DRY_RUN { + return nil + } + + // Reverse sort by index so we remove from the back + sort.Slice(mods, func(i, j int) bool { + return mods[j].idx < mods[i].idx + }) + + fmt.Println() + + var done int + for _, m := range mods { + fmt.Printf("Removing [%7d] %q - %q\n", m.fileId, m.name, m.file) + if err := cp.manifest.ArrayRemove(m.idx, "files"); err != nil { + log.Printf("Failed to remove mod %q from manifest\n", m.name) + continue + } + done++ + if m.file != "" { + if err := os.Remove(filepath.Join(cp.modPath(), m.file)); err != nil { + log.Println("Warning: ", err) + } + } + } + if done > 0 { + if err := cp.saveManifest(); err != nil { + return fmt.Errorf("failed to save changes to manifest") + } + } else { + return fmt.Errorf("failed to remove any mods; no changes have been made") + } + if done < len(mods) { + return fmt.Errorf("some mods could not be removed; pack may be in an invalid state") + } + + return nil +} + func cmdModRemoveSingle() error { // Try to open the mod pack cp, err := NewModPack(flag.Arg(1), true, ARG_MMC) @@ -459,57 +540,39 @@ func cmdModRemoveSingle() error { return err } - depInfo, err := _initRemove(cp, flag.Arg(2), 1) + depInfo, err := _initRemove(cp, flag.Args()[2:], 1) if err != nil { return err } - fmt.Println() - fmt.Println("Preparing to remove the mod:") - fmt.Printf("\t[%7d] %q (%s)\n", depInfo.target.fileId, depInfo.target.name, depInfo.target.file) + rmList := make([]*ManifestFileEntry, 0, len(depInfo.targets)) fmt.Println() - fmt.Println("The following mods depend on the mod being removed and will no longer work:") - for d := range depInfo.dependents { - fmt.Printf("\t[%7d] %q (%s)\n", d.fileId, d.name, d.file) + fmt.Println("Preparing to remove the mod(s):") + for target := range depInfo.targets { + fmt.Printf("\t[%7d] %q (%s)\n", target.fileId, target.name, target.file) + rmList = append(rmList, target) } fmt.Println() - fmt.Println("The following mods were added as a dependency for the mod being removed and are no longer required:") - for d, l := range depInfo.dependencies { - if len(l) == 1 && l[0] == depInfo.target { - fmt.Printf("\t[%7d] %q (%s)\n", d.fileId, d.name, d.file) - } + fmt.Println("The following mods depend on a mod being removed and will no longer work:") + for m, d := range depInfo.dependents { + fmt.Printf("\t[%7d] %q (%s)\n\t\tDepends on %s\n", m.fileId, m.name, m.file, QuoteJoin(d, ", ")) } fmt.Println() - fmt.Println("The following mods optionally depend on the mod being removed:") - for o, deps := range depInfo.optionals { - for _, d := range deps { - if d == depInfo.target { - fmt.Printf("\t[%7d] %q (%s)\n", o.fileId, o.name, o.file) - break - } - } + fmt.Println("The following mods were added as a dependency for a mod being removed and are no longer required:") + for m, d := range depInfo.dependencies { + fmt.Printf("\t[%7d] %q (%s)\n\t\tRequired by %s\n", m.fileId, m.name, m.file, QuoteJoin(d, ", ")) } - if !ARG_DRY_RUN { - fmt.Println() - fmt.Printf("Removing [%7d] %q - %q\n", depInfo.target.fileId, depInfo.target.name, depInfo.target.file) - if err := cp.manifest.ArrayRemove(depInfo.target.idx, "files"); err != nil { - return fmt.Errorf("failed to remove mod %q from manifest", depInfo.target.name) - } - if err := cp.saveManifest(); err != nil { - return fmt.Errorf("failed to save changes to manifest") - } - if depInfo.target.file != "" { - if err := os.Remove(filepath.Join(cp.modPath(), depInfo.target.file)); err != nil { - log.Println("Warning: ", err) - } - } + fmt.Println() + fmt.Println("The following mods optionally depend on a mod being removed:") + for d, o := range depInfo.optionals { + fmt.Printf("\t[%7d] %q optionally depends on %s\n", d.fileId, d.name, QuoteJoin(o, ", ")) } - return nil + return _removeMods(cp, rmList) } func cmdModRemoveRecursive() error { @@ -519,16 +582,18 @@ func cmdModRemoveRecursive() error { return err } - depInfo, err := _initRemove(cp, flag.Arg(2), -1) + depInfo, err := _initRemove(cp, flag.Args()[2:], -1) if err != nil { return err } fmt.Println() - fmt.Println("Preparing to remove the mod:") - fmt.Printf("\t[%7d] %q (%s)\n", depInfo.target.fileId, depInfo.target.name, depInfo.target.file) + fmt.Println("Preparing to remove the mod(s):") + for target := range depInfo.targets { + fmt.Printf("\t[%7d] %q (%s)\n", target.fileId, target.name, target.file) + } - rmList := []*ManifestFileEntry{depInfo.target} + rmList := make([]*ManifestFileEntry, len(depInfo.targets)+len(depInfo.dependencies)+len(depInfo.dependents)) fmt.Println() fmt.Println("The following dependent mods will also be removed:") @@ -551,41 +616,7 @@ func cmdModRemoveRecursive() error { rmList = append(rmList, d) } - if !ARG_DRY_RUN { - // Reverse sort by index so we remove from the back - sort.Slice(rmList, func(i, j int) bool { - return rmList[j].idx < rmList[i].idx - }) - - fmt.Println() - - var done int - for _, m := range rmList{ - fmt.Printf("Removing [%7d] %q - %q\n", m.fileId, m.name, m.file) - if err := cp.manifest.ArrayRemove(m.idx, "files"); err != nil { - log.Printf("Failed to remove mod %q from manifest\n", depInfo.target.name) - continue - } - done++ - if m.file != "" { - if err := os.Remove(filepath.Join(cp.modPath(), m.file)); err != nil { - log.Println("Warning: ", err) - } - } - } - if done > 0 { - if err := cp.saveManifest(); err != nil { - return fmt.Errorf("failed to save changes to manifest") - } - } else { - return fmt.Errorf("failed to remove any mods; no changes have been made") - } - if done < len(rmList) { - return fmt.Errorf("some mods could not be removed; pack may be in an invalid state") - } - } - - return nil + return _removeMods(cp, rmList) } From bf56bb0b26ceaa71684002b5965640643a9461d4 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Wed, 30 Jan 2019 21:10:58 -0700 Subject: [PATCH 4/6] Fix broken recursive removal --- main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 9f0ee98..560f1b3 100644 --- a/main.go +++ b/main.go @@ -587,14 +587,15 @@ func cmdModRemoveRecursive() error { return err } + rmList := make([]*ManifestFileEntry, 0, len(depInfo.targets)+len(depInfo.dependencies)+len(depInfo.dependents)) + fmt.Println() fmt.Println("Preparing to remove the mod(s):") for target := range depInfo.targets { fmt.Printf("\t[%7d] %q (%s)\n", target.fileId, target.name, target.file) + rmList = append(rmList, target) } - rmList := make([]*ManifestFileEntry, len(depInfo.targets)+len(depInfo.dependencies)+len(depInfo.dependents)) - fmt.Println() fmt.Println("The following dependent mods will also be removed:") for m, d := range depInfo.dependents { @@ -613,7 +614,6 @@ func cmdModRemoveRecursive() error { fmt.Println("The following mods optionally depend on a mod being removed:") for d, o := range depInfo.optionals { fmt.Printf("\t[%7d] %q optionally depends on %s\n", d.fileId, d.name, QuoteJoin(o, ", ")) - rmList = append(rmList, d) } return _removeMods(cp, rmList) From 2f7ed12446293077e0269a51834691d7db9516ca Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Sat, 2 Feb 2019 15:41:29 -0700 Subject: [PATCH 5/6] Use new install meta cache --- db.go | 8 +++++--- main.go | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/db.go b/db.go index 40c180b..604a047 100644 --- a/db.go +++ b/db.go @@ -384,12 +384,14 @@ func (db *Database) buildDepGraph(m *ModPack) (algo.Graph, error) { fileIds = make(map[int]*ManifestFileEntry, len(files)) for i, file := range files { record := ManifestFileEntry{idx: i} - if file.Exists("filename") { - record.file = file.S("filename").Data().(string) - } record.projId = int(file.S("projectID").Data().(float64)) record.fileId = int(file.S("fileID").Data().(float64)) + + if fid, filename := m.modCache.GetLastModFile(record.projId); fid > 0 { + record.file = filename + } + err = nameQuery.QueryRow(record.fileId, record.projId).Scan(&record.name) switch { case err == sql.ErrNoRows: diff --git a/main.go b/main.go index 560f1b3..a0922bb 100644 --- a/main.go +++ b/main.go @@ -513,10 +513,8 @@ func _removeMods(cp *ModPack, mods []*ManifestFileEntry) error { continue } done++ - if m.file != "" { - if err := os.Remove(filepath.Join(cp.modPath(), m.file)); err != nil { - log.Println("Warning: ", err) - } + if err := cp.modCache.CleanupModFile(m.projId); err != nil { + log.Println("Warning: ", err) } } if done > 0 { From 338077853b06563ea1e289072d18178d0c452d26 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Mon, 4 Feb 2019 11:59:10 -0700 Subject: [PATCH 6/6] Use new install meta cache for pack.show command --- modpack.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modpack.go b/modpack.go index f7a99a9..5a6e40c 100644 --- a/modpack.go +++ b/modpack.go @@ -619,12 +619,14 @@ func (pack *ModPack) getSelected(db *Database) ([]*ModDetails, error) { results := make([]*ModDetails, 0, len(files)) for _, file := range files { var record ModDetails - if file.Exists("filename") { - record.filename = file.S("filename").Data().(string) - } record.projectID = int(file.S("projectID").Data().(float64)) record.fileID = int(file.S("fileID").Data().(float64)) + + if fid, filename := pack.modCache.GetLastModFile(record.projectID); fid > 0 { + record.filename = filename + } + var ts int64 err = detailQuery.QueryRow(record.fileID, record.projectID).Scan(&record.name, &record.slug, &record.description, &ts) switch {