Refactor installer.lua to enhance functionality and improve user experience
This commit is contained in:
501
installer.lua
501
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' },
|
||||
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' },
|
||||
{ branch = 'master', description = '1.7x Stable' },
|
||||
{ branch = 'develop', description = '1.7x Unstable' },
|
||||
},
|
||||
}
|
||||
|
||||
preCopy = function(mode)
|
||||
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
|
||||
end,
|
||||
|
||||
steps = {
|
||||
install = {
|
||||
'splash',
|
||||
'license',
|
||||
'branch',
|
||||
'files',
|
||||
'review',
|
||||
'install',
|
||||
},
|
||||
update = {
|
||||
'branch',
|
||||
'review',
|
||||
'install',
|
||||
},
|
||||
automatic = {
|
||||
'install',
|
||||
},
|
||||
uninstall = {
|
||||
'review',
|
||||
'uninstall',
|
||||
},
|
||||
},
|
||||
}
|
||||
term.clear()
|
||||
drawHeader('Installing...')
|
||||
|
||||
print('Downloading Installer...')
|
||||
local _, h = term.getSize()
|
||||
local total = 0
|
||||
for _ in pairs(files) do total = total + 1 end
|
||||
|
||||
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')
|
||||
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
|
||||
|
||||
local contents = h.readAll()
|
||||
if not contents then
|
||||
error('Failed to download installer')
|
||||
-- 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
|
||||
|
||||
local fn, msg = load(contents, 'Installer.lua', nil, _ENV)
|
||||
if not fn then
|
||||
_G.printError(msg)
|
||||
else
|
||||
local args = { ... }
|
||||
fn(args[1])
|
||||
-- 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()
|
||||
|
||||
Reference in New Issue
Block a user