diff --git a/cmd/root.go b/cmd/root.go index f09f2d2..3536a8f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "github.com/nothub/mrpack-install/modrinth/mrpack" "github.com/nothub/mrpack-install/requester" "github.com/nothub/mrpack-install/server" + "github.com/nothub/mrpack-install/update" "github.com/nothub/mrpack-install/util" "github.com/spf13/cobra" "log" @@ -115,6 +116,12 @@ var rootCmd = &cobra.Command{ log.Fatalln(err) } archivePath = file + defer func(name string) { + err := os.Remove(name) + if err != nil { + fmt.Println(err) + } + }(archivePath) } else { // input is project id or slug? versions, err := modrinth.NewClient(host).GetProjectVersions(input, nil) @@ -155,6 +162,12 @@ var rootCmd = &cobra.Command{ if archivePath == "" { log.Fatalln("No mrpack file found for", input, version) } + defer func(name string) { + err := os.Remove(name) + if err != nil { + fmt.Println(err) + } + }(archivePath) } if archivePath == "" { @@ -233,6 +246,15 @@ var rootCmd = &cobra.Command{ log.Fatalln(err) } + info, err := update.GenerateModPackInfo(archivePath) + if err != nil { + fmt.Println(err) + } + err = info.Write(path.Join(serverDir, "modpack.json")) + if err != nil { + fmt.Println(err) + } + if modsUnclean { fmt.Println("There have been problems downloading downloading mods, you probably have to fix some dependency problems manually!") } diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 0000000..558aaa8 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,192 @@ +package cmd + +import ( + "fmt" + modrinth "github.com/nothub/mrpack-install/modrinth/api" + "github.com/nothub/mrpack-install/requester" + "github.com/nothub/mrpack-install/update" + "github.com/nothub/mrpack-install/util" + "github.com/spf13/cobra" + "log" + "os" + "path" + "strings" +) + +func init() { + + rootCmd.AddCommand(updateCmd) +} + +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Update the server", + Long: `Use file's hash and compare,Update the config and mods file'`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + err := cmd.Help() + if err != nil { + fmt.Println(err) + } + os.Exit(1) + } + input := args[0] + version := "" + if len(args) > 1 { + version = args[1] + } + + host, err := cmd.Flags().GetString("host") + if err != nil { + log.Fatalln(err) + } + serverDir, err := cmd.Flags().GetString("server-dir") + if err != nil { + log.Fatalln(err) + } + proxy, err := cmd.Flags().GetString("proxy") + if err != nil { + log.Fatalln(err) + } + if proxy != "" { + err := requester.DefaultHttpClient.SetProxy(proxy) + if err != nil { + log.Fatalln(err) + } + } + downloadThreads, err := cmd.Flags().GetInt("download-threads") + if err != nil || downloadThreads > 64 { + downloadThreads = 8 + fmt.Println(err) + } + retryTimes, err := cmd.Flags().GetInt("retry-times") + if err != nil { + retryTimes = 3 + fmt.Println(err) + } + + err = os.MkdirAll(serverDir, 0755) + if err != nil { + log.Fatalln(err) + } + + archivePath := "" + if util.PathIsFile(input) { + archivePath = input + + } else if util.IsValidUrl(input) { + fmt.Println("Downloading mrpack file from", args) + file, err := requester.DefaultHttpClient.DownloadFile(input, serverDir, "") + if err != nil { + log.Fatalln(err) + } + archivePath = file + defer func(name string) { + err := os.Remove(name) + if err != nil { + fmt.Println(err) + } + }(archivePath) + } else { // input is project id or slug? + versions, err := modrinth.NewClient(host).GetProjectVersions(input, nil) + if err != nil { + log.Fatalln(err) + } + + // get files uploaded for specified version or latest stable if not specified + var files []modrinth.File = nil + for i := range versions { + if version != "" { + if versions[i].VersionNumber == version { + files = versions[i].Files + break + } + } else { + if versions[i].VersionType == modrinth.ReleaseVersionType { + files = versions[i].Files + break + } + } + } + if len(files) == 0 { + log.Fatalln("No files found for", input, version) + } + + for i := range files { + if strings.HasSuffix(files[i].Filename, ".mrpack") { + fmt.Println("Downloading mrpack file from", files[i].Url) + file, err := requester.DefaultHttpClient.DownloadFile(files[i].Url, serverDir, "") + if err != nil { + log.Fatalln(err) + } + archivePath = file + break + } + } + if archivePath == "" { + log.Fatalln("No mrpack file found for", input, version) + } + defer func(name string) { + err := os.Remove(name) + if err != nil { + fmt.Println(err) + } + }(archivePath) + } + + if archivePath == "" { + log.Fatalln("An error occured!") + } + + fmt.Println("Processing mrpack file", archivePath) + + var downloads []*requester.Download + downloadPools := requester.NewDownloadPools(requester.DefaultHttpClient, downloads, downloadThreads, retryTimes) + + newModPackInfo, err := update.GenerateModPackInfo(archivePath) + if err != nil { + log.Fatalln(err) + } + err = newModPackInfo.Write(path.Join(serverDir, "modpack.json.update")) + if err != nil { + log.Fatalln(err) + } + oldModPackInfo, err := update.ReadModPackInfo(path.Join(serverDir, "modpack.json")) + if err != nil { + log.Fatalln(err) + } + deleteFileInfo, updateFileInfo, err := update.CompareModPackInfo(*oldModPackInfo, *newModPackInfo) + if err != nil { + return + } + deleteList := update.PreDelete(deleteFileInfo, serverDir) + updateList := update.PreUpdate(updateFileInfo, serverDir) + + fmt.Printf("Would you like to update: [y/N]") + var userInput string + _, err = fmt.Scanln(&userInput) + if err != nil { + log.Fatalln(err) + } + if userInput != "y" { + return + } + + err = update.ModPackDeleteDo(deleteList, serverDir) + if err != nil { + log.Fatalln(err) + } + err = update.ModPackUpdateDo(updateList, updateFileInfo.File, serverDir, archivePath, downloadPools) + if err != nil { + log.Fatalln(err) + } + util.RemoveEmptyDir(serverDir) + + err = os.Rename(path.Join(serverDir, "modpack.json.update"), path.Join(serverDir, "modpack.json")) + if err != nil { + log.Fatalln(err) + } + + fmt.Println("Done :) Have a nice day ✌️") + }, +} diff --git a/update/modPackInfoUtil.go b/update/modPackInfoUtil.go new file mode 100644 index 0000000..2b81561 --- /dev/null +++ b/update/modPackInfoUtil.go @@ -0,0 +1,96 @@ +package update + +import ( + "archive/zip" + "errors" + "fmt" + "github.com/nothub/mrpack-install/modrinth/mrpack" + "github.com/nothub/mrpack-install/util" + "reflect" + "strings" +) + +func GenerateModPackInfo(modPackPatch string) (*ModPackInfo, error) { + var modPackInfo ModPackInfo + modPackInfo.File = make(FileMap) + + modrinthIndex, err := mrpack.ReadIndex(modPackPatch) + if err != nil { + return nil, err + } + + modPackInfo.Dependencies = modrinthIndex.Dependencies + modPackInfo.ModPackVersion = modrinthIndex.VersionId + modPackInfo.ModPackName = modrinthIndex.Name + + // Add modrinth.index file + for _, file := range modrinthIndex.Files { + if file.Env.Server == "unsupported" { + continue + } + modPackInfo.File[Path(file.Path)] = FileInfo{Hash: string(file.Hashes.Sha1), DownloadLink: file.Downloads} + } + + // Add overrides file + r, err := zip.OpenReader(modPackPatch) + if err != nil { + return nil, err + } + defer func(r *zip.ReadCloser) { + err := r.Close() + if err != nil { + fmt.Println(err) + } + }(r) + + for _, f := range r.File { + if f.FileInfo().IsDir() { + continue + } + + filePath := f.Name + if strings.HasPrefix(filePath, "overrides/") { + filePath = strings.TrimPrefix(filePath, "overrides/") + } else if strings.HasPrefix(filePath, "server-overrides/") { + filePath = strings.TrimPrefix(filePath, "server-overrides/") + } else { + continue + } + + readCloser, err := f.Open() + if err != nil { + return nil, err + } + + fileHash, err := util.GetReadCloserSha1(readCloser) + if err != nil { + return nil, err + } + err = readCloser.Close() + if err != nil { + return nil, err + } + modPackInfo.File[Path(filePath)] = FileInfo{Hash: fileHash} + } + + return &modPackInfo, nil +} + +func CompareModPackInfo(oldVersion ModPackInfo, newVersion ModPackInfo) (deleteFileInfo *ModPackInfo, updateFileInfo *ModPackInfo, err error) { + if oldVersion.ModPackName != newVersion.ModPackName || !reflect.DeepEqual(oldVersion.Dependencies, newVersion.Dependencies) { + return nil, nil, errors.New("for mismatched versions, please upgrade manually") + } + + for path := range oldVersion.File { + if newVersion.File[path].Hash == oldVersion.File[path].Hash { + delete(oldVersion.File, path) + delete(newVersion.File, path) + } + + if _, ok := newVersion.File[path]; ok { + delete(oldVersion.File, path) + } + } + + return &oldVersion, &newVersion, nil +} diff --git a/update/model.go b/update/model.go new file mode 100644 index 0000000..789b88c --- /dev/null +++ b/update/model.go @@ -0,0 +1,57 @@ +package update + +import ( + "bytes" + "encoding/json" + "github.com/nothub/mrpack-install/modrinth/mrpack" + "os" +) + +type Path string +type FileMap map[Path]FileInfo + +type ModPackInfo struct { + ModPackVersion string `json:"modPackVersion"` + ModPackName string `json:"modPackName"` + File FileMap `json:"file"` + Dependencies mrpack.Dependencies `json:"dependencies"` +} + +type FileInfo struct { + Hash string `json:"Hash"` + DownloadLink []string `json:"DownloadLink"` +} + +func ReadModPackInfo(modPackJsonFile string) (*ModPackInfo, error) { + + var modPackJsonByte []byte + var err error + modPackJsonByte, err = os.ReadFile(modPackJsonFile) + if err != nil { + return nil, err + } + + modPackJson := ModPackInfo{} + err = json.Unmarshal(modPackJsonByte, &modPackJson) + + if err != nil { + return nil, err + } + return &modPackJson, nil +} + +func (modPackInfo *ModPackInfo) Write(modPackJsonFile string) error { + if modPackInfo != nil { + modPackJsonByte, err := json.Marshal(modPackInfo) + var out bytes.Buffer + err = json.Indent(&out, modPackJsonByte, "", "\t") + if err != nil { + return err + } + err = os.WriteFile(modPackJsonFile, out.Bytes(), 0644) + if err != nil { + return err + } + } + return nil +} diff --git a/update/update.go b/update/update.go new file mode 100644 index 0000000..cd41409 --- /dev/null +++ b/update/update.go @@ -0,0 +1,165 @@ +package update + +import ( + "archive/zip" + "fmt" + "github.com/nothub/mrpack-install/requester" + "github.com/nothub/mrpack-install/util" + "io" + "os" + "path/filepath" + "strings" +) + +type DetectList map[Path]util.DetectType + +// PreDelete Three scenarios +// 1.File does not exist Notice +// 2.File exists but hash value does not match,Change the original file name to xxx.bak +// 3.File exists and the hash value matches +func PreDelete(deleteList *ModPackInfo, serverPath string) DetectList { + detectType := make(DetectList, 10) + for filePath := range deleteList.File { + t := util.FileDetection(deleteList.File[filePath].Hash, filepath.Join(serverPath, string(filePath))) + switch t { + case util.PathMatchHashMatch: + fmt.Printf("[Delete]: %s \n", filePath) + detectType[filePath] = util.PathMatchHashMatch + case util.PathMatchHashNoMatch: + fmt.Printf("[Delete]: %s ,The original file will be move to updateBack folder\n", filePath) + detectType[filePath] = util.PathMatchHashNoMatch + } + } + return detectType +} + +// PreUpdate Three scenarios +// 1.File does not exist +// 2.File exists but hash value does not match,Change the original file name to xxx.bak +// 3.File exists and the hash value matches,Remove the item from the queue +func PreUpdate(updateList *ModPackInfo, serverPath string) DetectList { + detectType := make(DetectList, 10) + for filePath := range updateList.File { + switch util.FileDetection(updateList.File[filePath].Hash, filepath.Join(serverPath, string(filePath))) { + case util.PathMatchHashMatch: + delete(updateList.File, filePath) + case util.PathMatchHashNoMatch: + fmt.Printf("[Update]: %s ,The original file will be move to updateBack folder\n", filePath) + detectType[filePath] = util.PathMatchHashNoMatch + case util.PathNoMatch: + fmt.Printf("[Download]: %s \n", filePath) + detectType[filePath] = util.PathNoMatch + } + } + return detectType +} + +func ModPackDeleteDo(deleteList DetectList, serverPath string) error { + for filePath := range deleteList { + switch deleteList[filePath] { + case util.PathMatchHashMatch: + err := os.Remove(filepath.Join(serverPath, string(filePath))) + if err != nil { + return err + } + case util.PathMatchHashNoMatch: + err := os.MkdirAll(filepath.Dir(filepath.Join(serverPath, "updateBack", string(filePath))), 0755) + if err != nil { + return err + } + err = os.Rename(filepath.Join(serverPath, string(filePath)), filepath.Join(serverPath, "updateBack", string(filePath))) + if err != nil { + return err + } + } + } + return nil +} + +func ModPackUpdateDo(updateList DetectList, updateFileInfo FileMap, serverPath string, modPackPath string, downloadPools *requester.DownloadPools) error { + //backup file and download file in modrinth index + for filePath := range updateList { + switch updateList[filePath] { + case util.PathNoMatch: + if updateFileInfo[filePath].DownloadLink != nil { + downloadPools.Downloads = append(downloadPools.Downloads, requester.NewDownload(updateFileInfo[filePath].DownloadLink, map[string]string{"sha1": updateFileInfo[filePath].Hash}, filepath.Base(string(filePath)), filepath.Join(serverPath, filepath.Dir(string(filePath))))) + } + case util.PathMatchHashNoMatch: + err := os.MkdirAll(filepath.Dir(filepath.Join(serverPath, "updateBack", string(filePath))), 0755) + if err != nil { + return err + } + err = os.Rename(filepath.Join(serverPath, string(filePath)), filepath.Join(serverPath, "updateBack", string(filePath))) + if err != nil { + return err + } + if updateFileInfo[filePath].DownloadLink != nil { + downloadPools.Downloads = append(downloadPools.Downloads, requester.NewDownload(updateFileInfo[filePath].DownloadLink, map[string]string{"sha1": updateFileInfo[filePath].Hash}, filepath.Base(string(filePath)), filepath.Join(serverPath, filepath.Dir(string(filePath))))) + } + } + } + downloadPools.Do() + + // unzip update file + r, err := zip.OpenReader(modPackPath) + if err != nil { + return err + } + defer func(r *zip.ReadCloser) { + err := r.Close() + if err != nil { + fmt.Println(err) + } + }(r) + + for _, f := range r.File { + if f.FileInfo().IsDir() { + continue + } + + filePathInZip := f.Name + if strings.HasPrefix(filePathInZip, "overrides/") { + filePathInZip = strings.TrimPrefix(filePathInZip, "overrides/") + } else if strings.HasPrefix(filePathInZip, "server-overrides/") { + filePathInZip = strings.TrimPrefix(filePathInZip, "server-overrides/") + } else { + continue + } + + if _, ok := updateFileInfo[Path(filePathInZip)]; ok && updateFileInfo[Path(filePathInZip)].DownloadLink == nil { + + targetPath := filepath.Join(serverPath, filePathInZip) + + err := os.MkdirAll(filepath.Dir(targetPath), 0755) + if err != nil { + return err + } + + fileReader, err := f.Open() + if err != nil { + return err + } + + outFile, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + if _, err := io.Copy(outFile, fileReader); err != nil { + return err + } + + err = fileReader.Close() + if err != nil { + return err + } + err = outFile.Close() + if err != nil { + return err + } + + fmt.Println("Override file extracted:", targetPath) + + } + } + return nil +} diff --git a/util/checkSha1.go b/util/checkSha1.go deleted file mode 100644 index 93e24db..0000000 --- a/util/checkSha1.go +++ /dev/null @@ -1,50 +0,0 @@ -package util - -import ( - "crypto/sha1" - "encoding/hex" - "errors" - "fmt" - "io" - "net/http" - "os" -) - -func checkSha1(verifyHash string, verifyByte *[]byte) (bool, error) { - s := sha1.New() - s.Write(*verifyByte) - sha1Code := hex.EncodeToString(s.Sum(nil)) - if sha1Code == verifyHash { - return true, nil - } - return false, errors.New(fmt.Sprintf("data Hash Error,the data sha1 is %s,but you give hash is %s", sha1Code, verifyHash)) -} - -func CheckResponseSha1(verifyHash string, verifyResponse *http.Response) (bool, error) { - verifyByte, err := io.ReadAll(verifyResponse.Body) - if err != nil { - return false, err - } - err = verifyResponse.Body.Close() - if err != nil { - return false, err - } - verifyResponse.Body = io.NopCloser(verifyResponse.Body) - return checkSha1(verifyHash, &verifyByte) -} - -func CheckFileSha1(verifyHash string, verifyFile string) (bool, error) { - file, err := os.Open(verifyFile) - if err != nil { - return false, err - } - verifyByte, err := io.ReadAll(file) - if err != nil { - return false, err - } - err = file.Close() - if err != nil { - return false, err - } - return checkSha1(verifyHash, &verifyByte) -} diff --git a/util/file.go b/util/file.go index 7ca1c1e..1bc6e8c 100644 --- a/util/file.go +++ b/util/file.go @@ -1,6 +1,19 @@ package util -import "os" +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +type DetectType int8 + +const ( + PathMatchHashMatch DetectType = 0 + PathMatchHashNoMatch DetectType = 1 + PathNoMatch DetectType = 2 +) func PathIsFile(path string) bool { info, err := os.Stat(path) @@ -17,3 +30,43 @@ func PathIsDir(path string) bool { } return info.Mode().IsDir() } + +func FileDetection(hash string, path string) DetectType { + _, err := os.Stat(path) + if err != nil { + return PathNoMatch + } + if tmp, _ := CheckFileSha1(hash, path); tmp { + return PathMatchHashMatch + } else { + return PathMatchHashNoMatch + } +} + +func RemoveEmptyDir(dir string) { + fileNames := make([]string, 0) + dirNames := make([]string, 0) + + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + dirNames = append(dirNames, path) + } else { + fileNames = append(fileNames, path) + } + return err + }) + if err != nil { + panic(err) + } + + fileNamesAll := strings.Join(fileNames, "") + + for i := len(dirNames) - 1; i >= 0; i-- { + if !strings.Contains(fileNamesAll, dirNames[i]) { + err := os.Remove(dirNames[i]) + if err != nil { + fmt.Println(err) + } + } + } +} diff --git a/util/sha1.go b/util/sha1.go new file mode 100644 index 0000000..bcfcfe8 --- /dev/null +++ b/util/sha1.go @@ -0,0 +1,64 @@ +package util + +import ( + "crypto/sha1" + "encoding/hex" + "errors" + "fmt" + "io" + "log" + "os" +) + +func compareSha1(verifyHash string, newDataHash string) (bool, error) { + if newDataHash == verifyHash { + return true, nil + } + return false, errors.New(fmt.Sprintf("data Hash Error,the data sha1 is %s,but you give hash is %s", newDataHash, verifyHash)) +} + +func getSha1(newData *[]byte) string { + s := sha1.New() + s.Write(*newData) + sha1Code := hex.EncodeToString(s.Sum(nil)) + return sha1Code +} + +func CheckReadCloserSha1(verifyHash string, readCloser io.ReadCloser) (bool, error) { + newFileHash, err := GetReadCloserSha1(readCloser) + if err != nil { + return false, err + } + return compareSha1(verifyHash, newFileHash) +} + +func GetReadCloserSha1(readCloser io.ReadCloser) (string, error) { + verifyByte, err := io.ReadAll(readCloser) + if err != nil { + return "", err + } + return getSha1(&verifyByte), nil +} + +func CheckFileSha1(verifyHash string, verifyFile string) (bool, error) { + _, err := os.Stat(verifyFile) + if err != nil { + log.Fatalln("The validated file does not exist", verifyFile) + } + file, err := os.Open(verifyFile) + defer func(file *os.File) { + err := file.Close() + if err != nil { + fmt.Println(err) + } + }(file) + + if err != nil { + return false, err + } + newFileHash, err := GetReadCloserSha1(file) + if err != nil { + return false, err + } + return compareSha1(verifyHash, newFileHash) +}