UPDATE: Add precheck version hdiff, support another hdiff

This commit is contained in:
2025-08-29 21:13:10 +07:00
parent a4de02bc18
commit d58e76d821
12 changed files with 212 additions and 154 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -4,21 +4,19 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @param {number} timeout
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function CloseAppAfterTimeout(timeout) {
let $resultPromise = /** @type {any} */($Call.ByID(1705931481, timeout));
return $resultPromise;
return $Call.ByID(1705931481, timeout);
}
/**
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function GetCurrentLauncherVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(3575133982));
return $resultPromise;
return $Call.ByID(3575133982);
}

View File

@@ -4,70 +4,64 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @param {string} patchPath
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string, string]>}
*/
export function CheckTypeHDiff(patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(3717449114, patchPath));
return $resultPromise;
return $Call.ByID(3717449114, patchPath);
}
/**
* @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function CutData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(2019290107, gamePath));
return $resultPromise;
return $Call.ByID(2019290107, gamePath);
}
/**
* @param {string} gamePath
* @param {string} patchPath
* @param {boolean} isSkipVerify
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function DataExtract(gamePath, patchPath, isSkipVerify) {
let $resultPromise = /** @type {any} */($Call.ByID(2161622254, gamePath, patchPath, isSkipVerify));
return $resultPromise;
return $Call.ByID(2161622254, gamePath, patchPath, isSkipVerify);
}
/**
* @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function DeleteFiles(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(1103091613, gamePath));
return $resultPromise;
return $Call.ByID(1103091613, gamePath);
}
/**
* @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function HDiffPatchData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(3944051994, gamePath));
return $resultPromise;
return $Call.ByID(3944051994, gamePath);
}
/**
* @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function LDiffPatchData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(360123238, gamePath));
return $resultPromise;
return $Call.ByID(360123238, gamePath);
}
/**
* @param {string} gamePath
* @param {string} patchPath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @param {boolean} isNeedHDiff
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function VersionValidate(gamePath, patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(2105077257, gamePath, patchPath));
return $resultPromise;
export function VersionValidate(gamePath, patchPath, isNeedHDiff) {
return $Call.ByID(2105077257, gamePath, patchPath, isNeedHDiff);
}

View File

@@ -4,76 +4,68 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
* @returns {$CancellablePromise<boolean>}
*/
export function DirExists(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1772289644, path));
return $resultPromise;
return $Call.ByID(1772289644, path);
}
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
* @returns {$CancellablePromise<boolean>}
*/
export function FileExists(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1782610747, path));
return $resultPromise;
return $Call.ByID(1782610747, path);
}
/**
* @param {string} archivePath
* @param {string} fileInside
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function FileExistsInZip(archivePath, fileInside) {
let $resultPromise = /** @type {any} */($Call.ByID(2509699047, archivePath, fileInside));
return $resultPromise;
return $Call.ByID(2509699047, archivePath, fileInside);
}
/**
* @param {string} path
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function OpenFolder(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1635714001, path));
return $resultPromise;
return $Call.ByID(1635714001, path);
}
/**
* @param {string} filter
* @returns {Promise<string> & { cancel(): void }}
* @returns {$CancellablePromise<string>}
*/
export function PickFile(filter) {
let $resultPromise = /** @type {any} */($Call.ByID(3756474934, filter));
return $resultPromise;
return $Call.ByID(3756474934, filter);
}
/**
* @returns {Promise<string> & { cancel(): void }}
* @returns {$CancellablePromise<string>}
*/
export function PickFolder() {
let $resultPromise = /** @type {any} */($Call.ByID(3654471460));
return $resultPromise;
return $Call.ByID(3654471460);
}
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
* @returns {$CancellablePromise<boolean>}
*/
export function StartApp(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1267568402, path));
return $resultPromise;
return $Call.ByID(1267568402, path);
}
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
* @returns {$CancellablePromise<boolean>}
*/
export function StartWithConsole(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3249271428, path));
return $resultPromise;
return $Call.ByID(3249271428, path);
}

View File

@@ -4,71 +4,63 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function DownloadProxyProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(3559275334, version));
return $resultPromise;
return $Call.ByID(3559275334, version);
}
/**
* @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function DownloadServerProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(1954767259, version));
return $resultPromise;
return $Call.ByID(1954767259, version);
}
/**
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string, string]>}
*/
export function GetLatestLauncherVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(262637602));
return $resultPromise;
return $Call.ByID(262637602);
}
/**
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string, string]>}
*/
export function GetLatestProxyVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(289488362));
return $resultPromise;
return $Call.ByID(289488362);
}
/**
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string, string]>}
*/
export function GetLatestServerVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(2918980975));
return $resultPromise;
return $Call.ByID(2918980975);
}
/**
* @returns {Promise<void> & { cancel(): void }}
* @returns {$CancellablePromise<void>}
*/
export function UnzipProxy() {
let $resultPromise = /** @type {any} */($Call.ByID(2563246729));
return $resultPromise;
return $Call.ByID(2563246729);
}
/**
* @returns {Promise<void> & { cancel(): void }}
* @returns {$CancellablePromise<void>}
*/
export function UnzipServer() {
let $resultPromise = /** @type {any} */($Call.ByID(1126363284));
return $resultPromise;
return $Call.ByID(1126363284);
}
/**
* @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function UpdateLauncherProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(97272861, version));
return $resultPromise;
return $Call.ByID(97272861, version);
}

View File

@@ -4,24 +4,22 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @param {string} path
* @returns {Promise<[boolean, string, string, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string, string, string]>}
*/
export function GetLanguage(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3574191687, path));
return $resultPromise;
return $Call.ByID(3574191687, path);
}
/**
* @param {string} path
* @param {string} text
* @param {string} voice
* @returns {Promise<[boolean, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function SetLanguage(path, text, voice) {
let $resultPromise = /** @type {any} */($Call.ByID(2768939795, path, text, voice));
return $resultPromise;
return $Call.ByID(2768939795, path, text, voice);
}

View File

@@ -103,89 +103,80 @@ export default function DiffPage() {
}
const handleUpdateGame = async () => {
const handleResult = (ok: boolean, error: string) => {
if (!ok) {
toast.error(error)
return false
}
return true
}
try {
setIsDiffLoading(true)
if (!gameDir || !diffDir) {
toast.error('Please select game directory and diff file')
setIsDiffLoading(false)
return
}
setStageType('Check Type HDiff')
setProgressUpdate(0)
setMaxProgressUpdate(1)
const [isOk, validType, errorType] = await DiffService.CheckTypeHDiff(diffDir)
if (!isOk) {
toast.error(errorType)
setIsDiffLoading(false)
return
}
if (!handleResult(isOk, errorType)) return
setProgressUpdate(1)
if (validType === 'hdiffmap.json') {
if (['hdiffmap.json', 'hdifffiles.txt', 'hdifffiles.json'].includes(validType)) {
setStageType('Version Validate')
setProgressUpdate(0)
setMaxProgressUpdate(1)
const [validVersion, errorVersion] = await DiffService.VersionValidate(gameDir, diffDir)
if (!validVersion) {
toast.error(errorVersion)
setIsDiffLoading(false)
return
}
const isNeedHDiff = validType !== 'hdiffmap.json'
const [validVersion, errorVersion] = await DiffService.VersionValidate(gameDir, diffDir, isNeedHDiff)
if (!handleResult(validVersion, errorVersion)) return
setProgressUpdate(1)
}
const isSkipVerify = validType === 'manifest' || validType === 'hdifffiles.txt'
const isSkipVerify = ['manifest', 'hdifffiles.txt', 'hdifffiles.json'].includes(validType)
setStageType('Data Extract')
const [validData, errorData] = await DiffService.DataExtract(gameDir, diffDir, isSkipVerify)
if (!validData) {
toast.error(errorData)
setIsDiffLoading(false)
return
}
if (!handleResult(validData, errorData)) return
setStageType('Cut Data')
setMessageUpdate('')
const [validCut, errorCut] = await DiffService.CutData(gameDir)
if (!validCut) {
toast.error(errorCut)
setIsDiffLoading(false)
return
}
if ( validType === 'hdifffiles.txt' || validType === 'hdiffmap.json') {
setStageType('Patch Data')
const [validPatch, errorPatch] = await DiffService.HDiffPatchData(gameDir)
if (!validPatch) {
toast.error(errorPatch)
setIsDiffLoading(false)
return
if (!handleResult(validCut, errorCut)) return
switch (validType) {
case 'hdifffiles.txt':
case 'hdiffmap.json':
case 'hdifffiles.json': {
setStageType('Patch Data')
const [validPatch, errorPatch] = await DiffService.HDiffPatchData(gameDir)
if (!handleResult(validPatch, errorPatch)) return
setStageType('Delete old files')
const [validDelete, errorDelete] = await DiffService.DeleteFiles(gameDir)
if (!handleResult(validDelete, errorDelete)) return
break
}
setStageType('Delete old files')
const [validDelete, errorDelete] = await DiffService.DeleteFiles(gameDir)
if (!validDelete) {
toast.error(errorDelete)
setIsDiffLoading(false)
return
}
} else if (validType === 'manifest') {
setStageType('Patch Data')
const [validPatch, errorPatch] = await DiffService.LDiffPatchData(gameDir)
if (!validPatch) {
toast.error(errorPatch)
setIsDiffLoading(false)
return
case 'manifest': {
setStageType('Patch Data')
const [validPatch, errorPatch] = await DiffService.LDiffPatchData(gameDir)
if (!handleResult(validPatch, errorPatch)) return
break
}
}
toast.success('Update game completed')
} catch (err: any) {
console.error(err)
toast.error('PickFile error:', err)
setIsDiffLoading(false)
toast.error(`PickFile error: ${err}`)
} finally {
setIsDiffLoading(false)
}
}
return (
<div className="p-2 mx-4">

View File

@@ -7,6 +7,7 @@ import (
"firefly-launcher/pkg/hpatchz"
"firefly-launcher/pkg/models"
"firefly-launcher/pkg/sevenzip"
"firefly-launcher/pkg/verifier"
"fmt"
"os"
"path/filepath"
@@ -21,6 +22,9 @@ func (h *DiffService) CheckTypeHDiff(patchPath string) (bool, string, string) {
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok {
return true, "hdifffiles.txt", ""
}
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok {
return true, "hdifffiles.json", ""
}
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdiffmap.json"); err == nil && ok {
return true, "hdiffmap.json", ""
}
@@ -31,8 +35,9 @@ func (h *DiffService) CheckTypeHDiff(patchPath string) (bool, string, string) {
return false, "", "not found hdifffiles.txt or hdiffmap.json"
}
func (h *DiffService) VersionValidate(gamePath, patchPath string) (bool, string) {
oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes"))
func (h *DiffService) VersionValidate(gamePath, patchPath string, isNeedHDiff bool) (bool, string) {
oldBinPath := filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
oldVersionData, err := models.ParseBinaryVersion(oldBinPath)
if err != nil {
return false, err.Error()
}
@@ -46,28 +51,104 @@ func (h *DiffService) VersionValidate(gamePath, patchPath string) (bool, string)
return false, err.Error()
}
}
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes", constant.TempUrl); err != nil {
if !isNeedHDiff {
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes", constant.TempUrl); err != nil {
return false, err.Error()
}
binPath := filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
newVersionData, err := models.ParseBinaryVersion(binPath)
if err != nil {
return false, err.Error()
}
defer os.Remove(binPath)
v := newVersionData.Subtract(oldVersionData)
if v != 0 && v != 1 {
return false, fmt.Sprintf("the diff version %s not valid with game version %s", newVersionData, oldVersionData)
}
return true, "validated"
}
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes.hdiff", constant.TempUrl); err != nil {
return false, err.Error()
}
binPath := filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
newVersionData, err := models.ParseBinaryVersion(binPath)
patchBinFile := filepath.Join(constant.TempUrl, "BinaryVersion.bytes.hdiff")
sourceBinFile := filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
tempBinFile := filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
if err := hpatchz.ApplyPatch(sourceBinFile, patchBinFile, tempBinFile); err != nil {
os.Remove(patchBinFile)
return false, err.Error()
}
os.Remove(patchBinFile)
okFullPkg, err1 := sevenzip.IsFileIn7z(patchPath, "pkg_version")
okDiffPkg, err2 := sevenzip.IsFileIn7z(patchPath, "pkg_version.hdiff")
if err1 != nil && err2 != nil {
return false, err1.Error()
}
if okFullPkg {
if err := sevenzip.ExtractAFileFromZip(patchPath, "pkg_version", constant.TempUrl); err != nil {
return false, err.Error()
}
}
if okDiffPkg {
if err := sevenzip.ExtractAFileFromZip(patchPath, "pkg_version.hdiff", constant.TempUrl); err != nil {
return false, err.Error()
}
patchPkgFile := filepath.Join(constant.TempUrl, "pkg_version.hdiff")
sourcePkgFile := filepath.Join(gamePath, "pkg_version")
tempPkgFile := filepath.Join(constant.TempUrl, "pkg_version")
if err := hpatchz.ApplyPatch(sourcePkgFile, patchPkgFile, tempPkgFile); err != nil {
os.Remove(patchPkgFile)
os.Remove(tempPkgFile)
return false, err.Error()
}
os.Remove(patchPkgFile)
}
tempPkgFile := filepath.Join(constant.TempUrl, "pkg_version")
pkgDataList, err := models.LoadPkgVersion(tempPkgFile)
if err != nil {
os.Remove(tempPkgFile)
return false, err.Error()
}
defer os.Remove(binPath)
os.Remove(tempPkgFile)
v := newVersionData.Subtract(*oldVersionData)
if v != 0 && v != 1 {
return false, fmt.Sprintf("the diff version %s not valid with game version %s", newVersionData, oldVersionData)
for _, pkgData := range pkgDataList {
if filepath.ToSlash(pkgData.RemoteFile) == "StarRail_Data/StreamingAssets/BinaryVersion.bytes" {
md5, err := verifier.FileMD5(tempBinFile)
if err != nil {
os.Remove(tempBinFile)
return false, err.Error()
}
if md5 != pkgData.MD5 {
os.Remove(tempBinFile)
return false, fmt.Sprintf("md5 mismatch for %s: expected %s, got %s",
tempBinFile, pkgData.MD5, md5)
}
break
}
}
_, err = models.ParseBinaryVersion(tempBinFile)
if err != nil {
os.Remove(tempBinFile)
return false, err.Error()
}
os.Remove(tempBinFile)
return true, "validated"
}
func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
hdiffMapPath := filepath.Join(gamePath, "hdiffmap.json")
hdiffFilesPath := filepath.Join(gamePath, "hdifffiles.txt")
hdifffilesJsonPath := filepath.Join(gamePath, "hdifffiles.json")
var jsonData struct {
DiffMap []*models.HDiffData `json:"diff_map"`
@@ -88,6 +169,14 @@ func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
for _, entry := range jsonDataDiffMap.DiffMap {
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
}
} else if _, err := os.Stat(hdifffilesJsonPath); err == nil {
data, err := os.ReadFile(hdifffilesJsonPath)
if err != nil {
return false, err.Error()
}
if err := json.Unmarshal(data, &jsonData); err != nil {
return false, err.Error()
}
} else if _, err := os.Stat(hdiffFilesPath); err == nil {
files, err := models.LoadHDiffFiles(hdiffFilesPath)
if err != nil {
@@ -99,6 +188,7 @@ func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
} else {
return false, "no hdiff entries map exist"
}
application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Patching HDiff"})
for i, entry := range jsonData.DiffMap {
application.Get().Event.Emit(
@@ -134,6 +224,7 @@ func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
os.Remove(filepath.Join(gamePath, "hdiffmap.json"))
os.Remove(filepath.Join(gamePath, "hdifffiles.txt"))
os.Remove(filepath.Join(gamePath, "hdifffiles.json"))
return true, "patching completed"
}

View File

@@ -10,7 +10,7 @@ const ProxyZipFile = "64bit.zip"
const LauncherFile = "firefly-launcher.exe"
const TempUrl = "./temp"
const CurrentLauncherVersion = "1.6.0"
const CurrentLauncherVersion = "1.7.0"
type ToolFile string

View File

@@ -10,10 +10,11 @@ import (
)
type BinaryVersion struct {
Name string
Name string
Major int
Minor int
Patch int
Data []byte
}
func ParseBinaryVersion(path string) (*BinaryVersion, error) {
@@ -28,7 +29,7 @@ func ParseBinaryVersion(path string) (*BinaryVersion, error) {
if lastDash == -1 {
return nil, errors.New("no dash found in version string")
}
secondLastDash := strings.LastIndex(content[:lastDash], "-")
if secondLastDash == -1 {
return nil, errors.New("only one dash found in version string")
@@ -64,17 +65,18 @@ func ParseBinaryVersion(path string) (*BinaryVersion, error) {
}
}
binaryVersion.Data = data
return &binaryVersion, nil
}
func (v *BinaryVersion) String() string {
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
return fmt.Sprintf("%s-%d.%d.%d", v.Name, v.Major, v.Minor, v.Patch)
}
func (v BinaryVersion) ToInt() int {
return v.Major*100 + v.Minor*10 + v.Patch
}
func (v BinaryVersion) Subtract(other BinaryVersion) int {
func (v BinaryVersion) Subtract(other *BinaryVersion) int {
return v.ToInt() - other.ToInt()
}

View File

@@ -67,7 +67,7 @@ func check(relPath string, expectedSize int64, expectedMD5, base string) error {
fullPath, expectedSize, info.Size())
}
md5Hash, err := fileMD5(fullPath)
md5Hash, err := FileMD5(fullPath)
if err != nil {
return fmt.Errorf("error reading %s: %w", fullPath, err)
}
@@ -80,7 +80,7 @@ func check(relPath string, expectedSize int64, expectedMD5, base string) error {
return nil
}
func fileMD5(path string) (string, error) {
func FileMD5(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err