--[[ Opus OS Installer Self-contained installer that downloads from Gitea ]] local GITEA_HOST = 'git.spatulaa.com' local GITEA_USER = 'MayaTheShy' local GITEA_REPO = 'Opus' local GITEA_BRANCH = 'main' local TREE_URL = 'https://%s/api/v1/repos/%s/%s/git/trees/%s?recursive=1' local FILE_URL = 'https://%s/%s/%s/raw/branch/%s/%s' local colors = _G.colors local fs = _G.fs local http = _G.http local os = _G.os local parallel = _G.parallel local term = _G.term local textutils = _G.textutils local args = { ... } local mode = args[1] or 'install' -- Utility functions local function httpGet(url) local h, msg = http.get(url) if h then local content = h.readAll() h.close() return content end return nil, msg end local function download(url, path) local dir = fs.getDir(path) if dir ~= '' and not fs.exists(dir) then fs.makeDir(dir) end local content, msg = httpGet(url) if content then local f = fs.open(path, 'w') f.write(content) f.close() return true end return false, msg or 'Download failed' end local function formatSize(size) if size >= 1000000 then return string.format('%dM', math.floor(size / 1000000)) elseif size >= 1000 then return string.format('%dK', math.floor(size / 1000)) end return tostring(size) end local function center(text, y) local w = term.getSize() term.setCursorPos(math.floor((w - #text) / 2) + 1, y) term.write(text) end local function drawHeader(title) local w = term.getSize() term.setBackgroundColor(colors.cyan) term.setTextColor(colors.white) term.setCursorPos(1, 1) term.clearLine() center(title, 1) term.setBackgroundColor(colors.black) term.setTextColor(colors.white) end local function drawProgressBar(y, pct) local w = term.getSize() local barW = w - 4 local filled = math.floor(barW * pct / 100) term.setCursorPos(3, y) term.setBackgroundColor(colors.gray) term.write(string.rep(' ', barW)) term.setCursorPos(3, y) term.setBackgroundColor(colors.lime) term.write(string.rep(' ', filled)) term.setBackgroundColor(colors.black) end local function waitForKey(prompt) local _, h = term.getSize() if prompt then term.setTextColor(colors.lightGray) center(prompt, h) end os.pullEvent('key') end -- Get file listing from Gitea API local function getFileList(branch) local url = string.format(TREE_URL, GITEA_HOST, GITEA_USER, GITEA_REPO, branch) local content, msg = httpGet(url) if not content then return nil, 'Failed to get file list: ' .. (msg or 'unknown error') end local data = textutils.unserialiseJSON(content) if not data then return nil, 'Invalid response from server' end if data.message then return nil, data.message end local files = {} local totalSize = 0 for _, v in pairs(data.tree) do if v.type == 'blob' then local path = v.path:gsub('%s', '%%20') files[path] = { url = string.format(FILE_URL, GITEA_HOST, GITEA_USER, GITEA_REPO, branch, path), size = v.size or 0, } totalSize = totalSize + math.max(500, v.size or 0) end end return files, nil, totalSize end -- Splash screen local function showSplash() term.clear() drawHeader('Opus OS Installer') local _, h = term.getSize() local y = 3 term.setTextColor(colors.yellow) center('Opus OS v1.0', y) y = y + 2 term.setTextColor(colors.white) center('By: Kepler', y) y = y + 2 term.setTextColor(colors.lightGray) local desc = { 'A user friendly operating system', 'featuring multitasking, networking,', 'and automation.', } for _, line in ipairs(desc) do center(line, y) y = y + 1 end y = y + 1 term.setTextColor(colors.gray) center('Source: ' .. GITEA_HOST, y) waitForKey('Press any key to continue...') end -- License screen local function showLicense() term.clear() drawHeader('License Review') local _, h = term.getSize() term.setCursorPos(2, 3) term.setTextColor(colors.yellow) print(' Copyright (c) 2018 Kepler') print() term.setTextColor(colors.lightGray) local license = [[ Permission is hereby granted, free of charge, to any person obtaining a copy of this software to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.]] print(license) waitForKey('Press any key to accept...') end -- Branch selection (only shown for full install) local function selectBranch() local branches = { { branch = 'main', description = 'Main (Stable)' }, { branch = 'develop-1.8', description = '1.8+ Unstable' }, { branch = 'master-1.8', description = '1.8+ Stable' }, } local selected = 1 while true do term.clear() drawHeader('Select Branch') local _, h = term.getSize() term.setCursorPos(2, 3) term.setTextColor(colors.yellow) print(' Choose which branch to install:') print() for i, b in ipairs(branches) do term.setCursorPos(3, 4 + i) if i == selected then term.setTextColor(colors.yellow) term.write('> ') else term.setTextColor(colors.white) term.write(' ') end term.write(b.branch) term.setTextColor(colors.gray) term.write(' - ' .. b.description) end term.setTextColor(colors.lightGray) center('Up/Down to select, Enter to confirm', h) local event, key = os.pullEvent('key') if key == keys.up then selected = selected > 1 and selected - 1 or #branches elseif key == keys.down then selected = selected < #branches and selected + 1 or 1 elseif key == keys.enter then return branches[selected].branch end end end -- File list review local function showFileReview(files, totalSize) term.clear() drawHeader('Review Files') local _, h = term.getSize() local count = 0 for _ in pairs(files) do count = count + 1 end term.setCursorPos(2, 3) term.setTextColor(colors.yellow) print(string.format(' %d files to install', count)) print() term.setTextColor(colors.white) print(string.format(' Space required: %s', formatSize(totalSize))) local diskFree = fs.getFreeSpace('/') if totalSize > diskFree then term.setTextColor(colors.red) print(string.format(' Free space: %s (NOT ENOUGH!)', formatSize(diskFree))) else term.setTextColor(colors.lime) print(string.format(' Free space: %s', formatSize(diskFree))) end print() term.setTextColor(colors.lightGray) -- Show first few files local shown = 0 for path in pairs(files) do if shown < (h - 11) then term.setCursorPos(3, 9 + shown) local display = path local w = term.getSize() if #display > w - 4 then display = '...' .. display:sub(-(w - 7)) end term.write(display) shown = shown + 1 end end if count > shown then term.setCursorPos(3, 9 + shown) term.setTextColor(colors.gray) term.write(string.format('... and %d more files', count - shown)) end waitForKey('Press any key to begin install...') end -- Install files with progress local function installFiles(files) if mode == 'update' then fs.delete('sys') end term.clear() drawHeader('Installing...') local _, h = term.getSize() local total = 0 for _ in pairs(files) do total = total + 1 end local fileList = {} for path, entry in pairs(files) do table.insert(fileList, { path = path, url = entry.url }) end local completed = 0 local failed = 0 local lastFile = '' -- Download with 5 parallel workers local workers = {} for _ = 1, 5 do table.insert(workers, function() while true do local entry = table.remove(fileList) if not entry then break end lastFile = entry.path local s, m = download(entry.url, entry.path) if not s then failed = failed + 1 end completed = completed + 1 end end) end -- Progress display alongside downloads table.insert(workers, function() while completed < total do local pct = math.floor(completed / total * 100) term.setCursorPos(2, 4) term.setTextColor(colors.white) term.clearLine() term.write(string.format(' Progress: %d/%d (%d%%)', completed, total, pct)) term.setCursorPos(2, 6) term.setTextColor(colors.lightGray) term.clearLine() local w = term.getSize() local display = lastFile if #display > w - 4 then display = '...' .. display:sub(-(w - 7)) end term.write(' ' .. display) drawProgressBar(h - 3, pct) if failed > 0 then term.setCursorPos(2, 8) term.setTextColor(colors.red) term.write(string.format(' Failed: %d', failed)) end os.sleep(0.1) end end) parallel.waitForAll(table.unpack(workers)) -- Final progress update drawProgressBar(h - 3, 100) term.setCursorPos(2, 4) term.setTextColor(colors.lime) term.clearLine() term.write(string.format(' Complete: %d/%d files', completed, total)) term.setCursorPos(2, 6) term.clearLine() if failed > 0 then term.setCursorPos(2, 8) term.setTextColor(colors.red) term.write(string.format(' %d files failed to download', failed)) end return failed == 0 end -- Done screen local function showDone(success) local _, h = term.getSize() term.setCursorPos(2, h - 1) if success then term.setTextColor(colors.yellow) center('Install complete! Rebooting...', h - 1) os.sleep(2) os.reboot() else term.setTextColor(colors.red) center('Install finished with errors.', h - 1) waitForKey('Press any key to exit...') end end -- Main installer flow local function main() local branch = GITEA_BRANCH if mode == 'install' then showSplash() showLicense() branch = selectBranch() end -- Get file list term.clear() drawHeader('Opus OS Installer') term.setCursorPos(2, 4) term.setTextColor(colors.white) term.write(' Fetching file list...') local files, err, totalSize = getFileList(branch) if not files then term.setCursorPos(2, 6) term.setTextColor(colors.red) print(' Error: ' .. (err or 'unknown')) waitForKey('Press any key to exit...') return end if mode == 'install' then showFileReview(files, totalSize) end local success = installFiles(files) showDone(success) end main()