diff --git a/installer.lua b/installer.lua index d009d42..e4080d9 100644 --- a/installer.lua +++ b/installer.lua @@ -1,95 +1,448 @@ -install = { - title = 'Opus OS', - version = '1.0', - author = 'Kepler', +--[[ + Opus OS Installer + Self-contained installer that downloads from Gitea +]] - description = [[ -A user friendly operating system featuring multitasking, networking, -and automation -]], +local GITEA_HOST = 'git.spatulaa.com' +local GITEA_USER = 'MayaTheShy' +local GITEA_REPO = 'Opus' +local GITEA_BRANCH = 'main' - license = [[ -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and associated documentation -files (the "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, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: +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' -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +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 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE.]], +local args = { ... } +local mode = args[1] or 'install' - copyrightYear = 2018, - copyrightHolders = 'Kepler', +-- Utility functions - diskspace = 380000, - rebootAfter = true, - gitRepo = 'kepler155c/opus', +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 - gitBranch = 'main', - branches = { - { branch = 'main', description = 'Main' }, - { branch = 'develop-1.8', description = '1.8+ Unstable' }, - { branch = 'master-1.8', description = '1.8+ Stable' }, - { branch = 'master', description = '1.7x Stable' }, - { branch = 'develop', description = '1.7x Unstable' }, - }, +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 - preCopy = function(mode) - if mode == 'update' then - fs.delete('sys') +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, + end - steps = { - install = { - 'splash', - 'license', - 'branch', - 'files', - 'review', - 'install', - }, - update = { - 'branch', - 'review', - 'install', - }, - automatic = { - 'install', - }, - uninstall = { - 'review', - 'uninstall', - }, - }, -} - -print('Downloading Installer...') - -local url = 'https://raw.githubusercontent.com/kepler155c/opus-installer/master/sys/apps/Installer.lua' -local h = _G.http.get(url) -if not h then - error('Failed to download installer') + return files, nil, totalSize end -local contents = h.readAll() -if not contents then - error('Failed to download installer') +-- 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 -local fn, msg = load(contents, 'Installer.lua', nil, _ENV) -if not fn then - _G.printError(msg) -else - local args = { ... } - fn(args[1]) +-- 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()