Refactor installer.lua to enhance functionality and improve user experience
This commit is contained in:
513
installer.lua
513
installer.lua
@@ -1,95 +1,448 @@
|
|||||||
install = {
|
--[[
|
||||||
title = 'Opus OS',
|
Opus OS Installer
|
||||||
version = '1.0',
|
Self-contained installer that downloads from Gitea
|
||||||
author = 'Kepler',
|
]]
|
||||||
|
|
||||||
description = [[
|
local GITEA_HOST = 'git.spatulaa.com'
|
||||||
A user friendly operating system featuring multitasking, networking,
|
local GITEA_USER = 'MayaTheShy'
|
||||||
and automation
|
local GITEA_REPO = 'Opus'
|
||||||
]],
|
local GITEA_BRANCH = 'main'
|
||||||
|
|
||||||
license = [[
|
local TREE_URL = 'https://%s/api/v1/repos/%s/%s/git/trees/%s?recursive=1'
|
||||||
Permission is hereby granted, free of charge,
|
local FILE_URL = 'https://%s/%s/%s/raw/branch/%s/%s'
|
||||||
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:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
local colors = _G.colors
|
||||||
copies or substantial portions of the Software.
|
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
|
local args = { ... }
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
local mode = args[1] or 'install'
|
||||||
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.]],
|
|
||||||
|
|
||||||
copyrightYear = 2018,
|
-- Utility functions
|
||||||
copyrightHolders = 'Kepler',
|
|
||||||
|
|
||||||
diskspace = 380000,
|
local function httpGet(url)
|
||||||
rebootAfter = true,
|
local h, msg = http.get(url)
|
||||||
gitRepo = 'kepler155c/opus',
|
if h then
|
||||||
|
local content = h.readAll()
|
||||||
|
h.close()
|
||||||
|
return content
|
||||||
|
end
|
||||||
|
return nil, msg
|
||||||
|
end
|
||||||
|
|
||||||
gitBranch = 'main',
|
local function download(url, path)
|
||||||
branches = {
|
local dir = fs.getDir(path)
|
||||||
{ branch = 'main', description = 'Main' },
|
if dir ~= '' and not fs.exists(dir) then
|
||||||
{ branch = 'develop-1.8', description = '1.8+ Unstable' },
|
fs.makeDir(dir)
|
||||||
{ branch = 'master-1.8', description = '1.8+ Stable' },
|
end
|
||||||
{ branch = 'master', description = '1.7x Stable' },
|
local content, msg = httpGet(url)
|
||||||
{ branch = 'develop', description = '1.7x Unstable' },
|
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)
|
local function formatSize(size)
|
||||||
if mode == 'update' then
|
if size >= 1000000 then
|
||||||
fs.delete('sys')
|
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,
|
end
|
||||||
|
|
||||||
steps = {
|
return files, nil, totalSize
|
||||||
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')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local contents = h.readAll()
|
-- Splash screen
|
||||||
if not contents then
|
|
||||||
error('Failed to download installer')
|
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
|
end
|
||||||
|
|
||||||
local fn, msg = load(contents, 'Installer.lua', nil, _ENV)
|
-- License screen
|
||||||
if not fn then
|
|
||||||
_G.printError(msg)
|
local function showLicense()
|
||||||
else
|
term.clear()
|
||||||
local args = { ... }
|
drawHeader('License Review')
|
||||||
fn(args[1])
|
|
||||||
|
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
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user