449 lines
10 KiB
Lua
449 lines
10 KiB
Lua
--[[
|
|
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()
|