/ install.lua
install.lua
  1  --[[
  2      ToasterGen Spin
  3  
  4      Copyright (C) 2025 Clifton Toaster Reid <cliftontreid@duck.com>
  5  
  6      This library is free software: you can redistribute it and/or modify
  7      it under the terms of the GNU Lesser General Public License as published by
  8      the Free Software Foundation, either version 3 of the License, or
  9      (at your option) any later version.
 10  ]]
 11  
 12  -- Configuration
 13  local CONFIG = {
 14  	GITHUB_USER = "cliftontoaster-reid",
 15  	GITHUB_REPO = "cc-roulette",
 16  	TOOLS_DIR = "/tools",
 17  	ROULETTE_DIR = "/tools/roulette",
 18  	ARCHIVE_DIR = "/tools/cc-archive",
 19  	TEMP_DIR = "/tmp",
 20  	LOG_FILE = "/tools/roulette-install.log",
 21  	LOG_LEVEL = "DEBUG", -- DEBUG, INFO, WARNING, ERROR
 22  	ARCHIVE_FILES = {
 23  		"LibDeflate.lua",
 24  		"ar.lua",
 25  		"archive.lua",
 26  		"arlib.lua",
 27  		"gzip.lua",
 28  		"muxzcat.lua",
 29  		"tar.lua",
 30  		"unxz.lua",
 31  	},
 32  	VERSION_FILE = "/tools/roulette/version",
 33  }
 34  
 35  local Logger = require("src.log")
 36  local semver = require("src.semver")
 37  
 38  local args = { ... }
 39  local forceDev = false
 40  local forceDel = false
 41  local quietRun = false
 42  local logLevel = CONFIG.LOG_LEVEL
 43  for i, arg in ipairs(args) do
 44  	if arg == "--dev" then
 45  		forceDev = true
 46  	elseif arg == "--del" then
 47  		forceDel = true
 48  	elseif arg == "--quiet" then
 49  		quietRun = true
 50  	elseif arg == "--log-level" then
 51  		logLevel = args[i + 1] or logLevel
 52  	end
 53  end
 54  
 55  Logger.setLogLevel(logLevel)
 56  
 57  --- Fetches the releases for a given GitHub repository.
 58  --- @param githubUser string The GitHub username of the repository owner.
 59  --- @param githubRepo string The GitHub repository name.
 60  --- @param logger Logger The logger to use for output.
 61  --- @return Release[]|nil The releases for the repository, or nil if an error occurred.
 62  local function getReleases(githubUser, githubRepo, logger)
 63  	local url = string.format("https://api.github.com/repos/%s/%s/releases", githubUser, githubRepo)
 64  	logger.info("Fetching releases from GitHub API...")
 65  	local response = http.get(url)
 66  	if not response then
 67  		logger.error("Failed to fetch releases from GitHub API")
 68  		return nil
 69  	end
 70  	local data = response.readAll()
 71  	response.close()
 72  	logger.debug("Received " .. #data .. " bytes of release data")
 73  	return textutils.unserializeJSON(data)
 74  end
 75  
 76  -- Utility functions
 77  local function printHeader(text)
 78  	local oldColor = term.getTextColor()
 79  	term.setTextColor(colors.cyan)
 80  	print("======================================")
 81  	print(text)
 82  	print("======================================")
 83  	term.setTextColor(oldColor)
 84  	Logger.debug("Header displayed: " .. text)
 85  end
 86  
 87  local function printFooter()
 88  	local oldColor = term.getTextColor()
 89  	term.setTextColor(colors.cyan)
 90  	print("======================================")
 91  	term.setTextColor(oldColor)
 92  	Logger.debug("Footer displayed")
 93  end
 94  
 95  local function ensureDirectory(path)
 96  	if not fs.exists(path) then
 97  		fs.makeDir(path)
 98  		Logger.debug("Created directory: " .. path)
 99  		return true
100  	end
101  	Logger.debug("Directory already exists: " .. path)
102  	return false
103  end
104  
105  local function downloadFile(url, path, binary)
106  	Logger.debug("Downloading file from " .. url .. " to " .. path)
107  	local response = http.get(url, nil, binary)
108  	if not response then
109  		Logger.error("Failed to download from " .. url)
110  		return false, "Failed to download from " .. url
111  	end
112  
113  	local data = response.readAll()
114  	response.close()
115  
116  	local file = fs.open(path, "wb")
117  	file.write(data)
118  	file.close()
119  
120  	Logger.debug("Successfully downloaded file to " .. path)
121  	return true
122  end
123  
124  local function downloadCCArchive()
125  	if fs.exists(CONFIG.ARCHIVE_DIR) then
126  		Logger.info("CC-Archive already exists at " .. CONFIG.ARCHIVE_DIR)
127  		return true
128  	end
129  
130  	Logger.info("Downloading CC-Archive...")
131  	ensureDirectory(CONFIG.TOOLS_DIR)
132  	ensureDirectory(CONFIG.ARCHIVE_DIR)
133  
134  	local baseUrl = "https://github.com/MCJack123/CC-Archive/raw/refs/heads/master/"
135  	local totalFiles = #CONFIG.ARCHIVE_FILES
136  
137  	for i, file in ipairs(CONFIG.ARCHIVE_FILES) do
138  		Logger.info(string.format("[%d/%d] Downloading %s", i, totalFiles, file))
139  		local success, err = downloadFile(baseUrl .. file, CONFIG.ARCHIVE_DIR .. "/" .. file)
140  		if not success then
141  			Logger.error("Failed to download " .. file .. ": " .. error)
142  			error(err)
143  		end
144  	end
145  
146  	Logger.success("Successfully downloaded all CC-Archive components")
147  	return true
148  end
149  
150  local function downloadAndExtractRelease(release)
151  	Logger.info("Starting download of release " .. release.tag_name)
152  	ensureDirectory(CONFIG.TEMP_DIR)
153  
154  	local tempFile = CONFIG.TEMP_DIR .. "/release.tar.gz"
155  	if fs.exists(tempFile) then
156  		Logger.debug("Removing existing temp file: " .. tempFile)
157  		fs.delete(tempFile)
158  	end
159  
160  	Logger.info("Downloading release archive...")
161  	local success, err = downloadFile(release.tarball_url, tempFile, true)
162  	if not success then
163  		Logger.error("Failed to download release archive: " .. err)
164  		error(err)
165  	end
166  
167  	Logger.info("Extracting release files...")
168  	ensureDirectory(CONFIG.ROULETTE_DIR)
169  
170  	Logger.debug("Running tar command to extract files")
171  	shell.run(CONFIG.ARCHIVE_DIR .. "/tar.lua", "xzf", tempFile, "-C", CONFIG.ROULETTE_DIR)
172  
173  	-- Handle nested directory
174  	local files = fs.list(CONFIG.ROULETTE_DIR)
175  	if #files == 1 and fs.isDir(CONFIG.ROULETTE_DIR .. "/" .. files[1]) then
176  		local folder = files[1]
177  		local folderPath = CONFIG.ROULETTE_DIR .. "/" .. folder
178  
179  		Logger.info("Organizing extracted files from nested directory: " .. folder)
180  		local fileCount = 0
181  		for _, file in ipairs(fs.list(folderPath)) do
182  			fs.move(folderPath .. "/" .. file, CONFIG.ROULETTE_DIR .. "/" .. file)
183  			fileCount = fileCount + 1
184  		end
185  
186  		Logger.debug("Moved " .. fileCount .. " files to target directory")
187  		Logger.debug("Removing temporary directory: " .. folderPath)
188  		fs.delete(folderPath)
189  	end
190  
191  	Logger.success("Release extraction completed successfully")
192  	return true
193  end
194  
195  local function displayReleaseSummary(release)
196  	printHeader("Release Summary for " .. CONFIG.GITHUB_REPO)
197  
198  	Logger.info("Version: " .. release.tag_name)
199  	Logger.info("Name: " .. (release.name or "Unnamed"))
200  	Logger.info("Published: " .. (release.published_at or release.created_at or "Unknown"))
201  
202  	local status = release.draft and "Draft" or (release.prerelease and "Pre-release" or "Stable")
203  	Logger.info("Status: " .. status)
204  	Logger.info("Assets: " .. #release.assets)
205  
206  	if release.body then
207  		Logger.info("Description:")
208  		local shortDesc = string.sub(release.body, 1, 100)
209  		if #release.body > 100 then
210  			shortDesc = shortDesc .. "..."
211  		end
212  		Logger.info(shortDesc)
213  	end
214  
215  	printFooter()
216  end
217  
218  -- Main execution
219  local function main()
220  	if forceDel then
221  		Logger.info("Force deletion (--del) detected. Removing entire tools directory: " .. CONFIG.TOOLS_DIR)
222  		if fs.exists(CONFIG.TOOLS_DIR) then
223  			fs.delete(CONFIG.TOOLS_DIR)
224  			Logger.success("Successfully removed " .. CONFIG.TOOLS_DIR)
225  		else
226  			Logger.warning("No tools directory found at " .. CONFIG.TOOLS_DIR)
227  		end
228  	end
229  
230  	local update = false
231  
232  	Logger.info("Starting installation of " .. CONFIG.GITHUB_REPO .. "...")
233  
234  	-- If the --dev flag is present, force install from the provided URL
235  	if forceDev then
236  		Logger.info("Force installation (--dev) detected. Forcing installation from dev release.")
237  
238  		local latest = {
239  			tag_name = "vDEV",
240  			tarball_url = "https://github.com/cliftontoaster-reid/cc-roulette/archive/main.tar.gz",
241  		}
242  
243  		if fs.exists(CONFIG.ROULETTE_DIR) then
244  			Logger.info("Removing existing installation due to --dev flag")
245  			fs.delete(CONFIG.ROULETTE_DIR)
246  		end
247  
248  		downloadCCArchive()
249  		downloadAndExtractRelease(latest)
250  
251  		printHeader("Installation complete")
252  		Logger.success("Installation (dev) of " .. CONFIG.GITHUB_REPO .. " completed successfully")
253  
254  		local versionFile = fs.open(CONFIG.VERSION_FILE, "w")
255  		versionFile.writeLine(latest.tag_name:sub(2))
256  		versionFile.close()
257  
258  		local config = require("tools.config")
259  		if config.askYesNo("Would you like to configure the program now?") then
260  			local dev = config.askOption("What device would you like to configure?", { "Table", "Server" })
261  			if dev == "Table" then
262  				config.configTable()
263  			elseif dev == "Server" then
264  				error("Server configuration not yet implemented, we apologize for the inconvenience")
265  			end
266  		end
267  
268  		Logger.debug("Exiting the program (dev mode)...")
269  		return true
270  	end
271  
272  	local releases = getReleases(CONFIG.GITHUB_USER, CONFIG.GITHUB_REPO, Logger)
273  
274  	if not releases or #releases == 0 then
275  		Logger.error("No releases found for repository")
276  		return
277  	end
278  
279  	local latest = releases[1]
280  	Logger.info("Found latest release: " .. latest.tag_name)
281  
282  	displayReleaseSummary(latest)
283  
284  	if fs.exists(CONFIG.ROULETTE_DIR) then
285  		update = true
286  		Logger.info("Existing installation found at " .. CONFIG.ROULETTE_DIR)
287  
288  		local versionFile = fs.open(CONFIG.VERSION_FILE, "r")
289  		if versionFile then
290  			local currentVersion = versionFile.readLine()
291  			versionFile.close()
292  
293  			local cVer = semver.parse(currentVersion)
294  			local lVer = semver.parse(latest.tag_name:sub(2))
295  
296  			Logger.debug("Installed version: " .. currentVersion)
297  			Logger.debug("Latest version: " .. latest.tag_name)
298  			if not cVer or not lVer or semver.ge(cVer, lVer) then
299  				Logger.success("Installed version is up to date")
300  				return
301  			else
302  				Logger.info("Updating from version " .. currentVersion .. " to " .. latest.tag_name)
303  				fs.delete(CONFIG.ROULETTE_DIR)
304  			end
305  		else
306  			Logger.warning("Failed to read version file, continuing with installation")
307  			fs.delete(CONFIG.ROULETTE_DIR)
308  		end
309  	end
310  
311  	downloadCCArchive()
312  	downloadAndExtractRelease(latest)
313  
314  	printHeader("Installation complete")
315  	Logger.success("Installation of " .. CONFIG.GITHUB_REPO .. " completed successfully")
316  
317  	local versionFile = fs.open(CONFIG.VERSION_FILE, "w")
318  	versionFile.writeLine(latest.tag_name:sub(2))
319  	versionFile.close()
320  
321  	local config = require("tools.config")
322  
323  	if not update or not quietRun then
324  		if config.askYesNo("Would you like to configure the program now?") then
325  			local dev = config.askOption("What device would you like to configure?", { "Table", "Server" })
326  			if dev == "Table" then
327  				config.configTable()
328  			elseif dev == "Server" then
329  				error("Server configuration not yet implemented, we apologize for the inconvenience")
330  			end
331  		end
332  	end
333  
334  	Logger.debug("Exiting the program...")
335  	return true
336  end
337  
338  -- Run the program
339  main()