--libAutoUpdater.lua ---@class LibAutoUpdaterConfig ---@field repo string ---@field branch string ---@field debug boolean ---@field autorun boolean ---@field doRestart boolean local config = ... local json = require("main.json") ---@class AutoUpdater local AutoUpdater = { hasFailed = false, currentStep = 0, ---@type string gitHash = nil, ---@type string latestHash = nil, ---@type string artifactId = nil, ---@type string downloadUrl = nil, ---@type boolean isSilent = false, } ---@type {[integer]: {text: string, method: function}} local steps = { [1] = { text = "Getting current version...", method = function() AutoUpdater.getCurrentVersion() end, }, [2] = { text = "Getting latest build artifact...", method = function() AutoUpdater.getArtifactInfo() end, }, [3] = { text = "Getting version info...", method = function() AutoUpdater.getCommitInfo() end, }, [4] = { text = "Requesting download URL...", method = function() AutoUpdater.getDownloadUrl() end, }, [5] = { text = "Downloading latest build artifact...", method = function() AutoUpdater.downloadArtifact() end, }, [6] = { text = "Unzipping latest build artifact...", method = function() AutoUpdater.unzipArtifact() end, }, [7] = { text = "Moving files...", method = function() AutoUpdater.moveFiles() end, }, [8] = { text = "Cleaning up...", method = function() AutoUpdater.cleanup() end, }, } ---@param silent boolean? function AutoUpdater.run(silent) AutoUpdater.isSilent = silent or false AutoUpdater.print("Checking for updates...") AutoUpdater.nextStep() end function AutoUpdater.nextStep() if AutoUpdater.hasFailed then return end AutoUpdater.currentStep = AutoUpdater.currentStep + 1 if steps[AutoUpdater.currentStep] then AutoUpdater.debug(steps[AutoUpdater.currentStep].text) steps[AutoUpdater.currentStep].method() else if config.doRestart then AutoUpdater.print("Update complete. Restarting server...") os.exit() else AutoUpdater.print("Update complete.") AutoUpdater.print("Please restart the server to apply changes.") end end end ---@param reason string function AutoUpdater.fail(reason) AutoUpdater.print("Update failed: " .. reason) AutoUpdater.hasFailed = true end function AutoUpdater.print(text) if AutoUpdater.isSilent then return end print("\27[30;1m[" .. os.date("%X") .. "]\27[0m \27[38;5;219m[AutoUpdater]\27[0m " .. text) end function AutoUpdater.debug(text) if config.debug and not AutoUpdater.isSilent then AutoUpdater.print(text) end end ---Searches for a string in a binary file. ---@param filePath string The path to the file. ---@param value string The string to search for. ---@return integer The position of the string in the file, or -1 if not found. function AutoUpdater.BinaryStringSearch(filePath, value) local f = io.open(filePath, "r") if f then ---@type string local content = f:read("*all") f:close() local pos = content:find(value, 1, true) if pos then return pos end end return -1 end function AutoUpdater.getCurrentVersion() local pos = AutoUpdater.BinaryStringSearch("./librosaserver.so", "git version:") if pos == -1 then AutoUpdater.print("Failed to find git hash, updating...") AutoUpdater.nextStep() return end AutoUpdater.debug(string.format("Found git hash at position 0x%X", pos)) local f = io.open("./librosaserver.so", "r") if f then f:seek("set", pos + 12) AutoUpdater.gitHash = f:read(40) f:close() local gitHashShort = AutoUpdater.gitHash:sub(1, 7) AutoUpdater.debug("Current version: " .. gitHashShort) else AutoUpdater.fail("Failed to open librosaserver.so") end AutoUpdater.nextStep() end function AutoUpdater.getArtifactInfo() http.get("https://api.github.com", "/repos/" .. config.repo .. "/actions/artifacts", { ["Accept"] = "application/vnd.github.v3+json", }, function(res) if res and res.status == 200 then local data = json.decode(res.body) local latestArtifact = nil for _, artifact in pairs(data.artifacts) do if artifact.workflow_run.head_branch == config.branch then if not latestArtifact or artifact.created_at > latestArtifact.created_at then latestArtifact = artifact AutoUpdater.debug("Found artifact: " .. artifact.id) end end end if latestArtifact then local artifactId = latestArtifact.id local artifactName = latestArtifact.name local artifactSize = latestArtifact.size_in_bytes local headSha = latestArtifact.workflow_run.head_sha AutoUpdater.debug( string.format( "Latest artifact: %s - %s (%s)", artifactName, artifactId, AutoUpdater.formatDataSize(artifactSize) ) ) if headSha == AutoUpdater.gitHash or config.forceUpdate then AutoUpdater.print("Already up to date.") AutoUpdater.hasFailed = true return end AutoUpdater.debug("Latest version: " .. headSha:sub(1, 7)) AutoUpdater.latestHash = headSha AutoUpdater.artifactId = artifactId else AutoUpdater.fail("Failed to get latest artifact: No artifacts found") end else AutoUpdater.fail("Failed to get latest artifact: " .. (res and res.status or "500")) end AutoUpdater.nextStep() end) end function AutoUpdater.getCommitInfo() http.get("https://api.github.com", "/repos/" .. config.repo .. "/commits/" .. AutoUpdater.latestHash, { ["Accept"] = "application/vnd.github.v3+json", }, function(res) if res and res.status == 200 then local data = json.decode(res.body) local latestCommit = data.sha if latestCommit ~= AutoUpdater.gitHash and not config.forceUpdate then AutoUpdater.latestHash = latestCommit local commitInfo = data.commit.committer local commitDate = commitInfo.date local commitAuthor = commitInfo.name local commitMessage = data.commit.message AutoUpdater.debug("Latest commit: " .. commitDate .. " by " .. commitAuthor .. " - " .. commitMessage) else AutoUpdater.print("Already up to date, current commit: " .. commitDate .. " by " .. commitAuthor .. " - " .. commitMessage) AutoUpdater.hasFailed = true end else AutoUpdater.fail("Failed to get latest version: " .. (res and res.status or "500")) end AutoUpdater.nextStep() end) end function AutoUpdater.getDownloadUrl() http.get("https://jpxs.io", "/api/install/artifact/RosaServer/" .. AutoUpdater.artifactId .. "/zip", { ["Accept"] = "application/json", }, function(res) if res and res.status == 200 then local data = json.decode(res.body) local downloadUrl = data.location AutoUpdater.downloadUrl = downloadUrl AutoUpdater.debug("Download URL: " .. downloadUrl) AutoUpdater.nextStep() else AutoUpdater.fail("Failed to download artifact: " .. (res and res.status or "500")) AutoUpdater.nextStep() end end) end function AutoUpdater.downloadArtifact() local host = AutoUpdater.downloadUrl:match("(https?://[^/]+)") local path = AutoUpdater.downloadUrl:match("https?://[^/]+(/.*)") http.get(host, path, { ["Accept"] = "application/zip", }, function(res) if res and res.status == 200 then if os.createDirectory(AutoUpdater.latestHash) then AutoUpdater.debug("Created directory for new version.") end local f = io.open(AutoUpdater.latestHash .. "/update.zip", "w") if f then f:write(res.body) f:close() AutoUpdater.print("Download complete.") AutoUpdater.nextStep() else AutoUpdater.fail("Failed to write to file. Please check permissions.") end else AutoUpdater.fail("Failed to download artifact: " .. (res and res.status or "500")) end end) end function AutoUpdater.unzipArtifact() local command = string.format("unzip -o %s/update.zip -d %s", AutoUpdater.latestHash, AutoUpdater.latestHash) os.execute(command) AutoUpdater.print("Unzipped latest build artifact.") AutoUpdater.nextStep() end function AutoUpdater.moveFiles() local baseDir = AutoUpdater.latestHash .. "/" os.execute("mv " .. baseDir .. "librosaserver.so ./librosaserver.so") os.execute("mv " .. baseDir .. "rosaserversatellite ./rosaserversatellite") os.execute("mv " .. baseDir .. "libluajit.so ./libluajit.so") AutoUpdater.print("Moved files.") AutoUpdater.nextStep() end function AutoUpdater.cleanup() os.execute("rm -rf " .. AutoUpdater.latestHash) AutoUpdater.print("Cleaned up.") AutoUpdater.nextStep() end function AutoUpdater.formatDataSize(size) local units = { "B", "KB", "MB", "GB", "TB" } local unit = 1 while size > 1024 do size = size / 1024 unit = unit + 1 end return string.format("%.2f %s", size, units[unit]) end return AutoUpdater