ComputerCraft Archive

ccs get

computer operating-system Trinitek github

Description

ComputerCraftScripts-Get: An installer and updater.

Installation

Copy one of these commands into your ComputerCraft terminal:

wget:wget https://raw.githubusercontent.com/Trinitek/ComputerCraftScripts/master/ccs-get.lua ccs_get
Archive:wget https://cc.shobie.xyz/cc/get/gh-Trinitek-ComputerCraftScripts-ccs-get ccs_get
Quick Install: wget https://cc.shobie.xyz/cc/get/gh-Trinitek-ComputerCraftScripts-ccs-get ccs get

Usage

Run: ccs-get

Tags

none

Source

View Original Source

Code Preview

--- ComputerCraftScripts-Get: An installer and updater.

-- Arguments:
-- -ssu   Suppress self-updater
-- -f     Force content-update

local updaterProgramName = "ccs-get"
local ccsDirectory = "ccs"
local updaterUrl = "https://raw.githubusercontent.com/Trinitek/ComputerCraftScripts/master/ccs-get.lua"
local githubContentRoot = "https://api.github.com/repos/Trinitek/ComputerCraftScripts/contents"
local githubCommitsRoot = "https://api.github.com/repos/Trinitek/ComputerCraftScripts/commits?per_page=1"
local githubScriptsDirectory = "scripts"
local lockFileName = ".ccs-get.lock"

---@param message any
---@param color number
local function printColor(message, color)
    local originalColor = term.getTextColor();
    term.setTextColor(color);
    print(message);
    term.setTextColor(originalColor);
end

---@class Lockfile
---@field latestCommitSha string?
---@field latestCommitDate string?

---@return string
local function getLockfilePath()
    return lockFileName;
end

---@return Lockfile
local function readLockfile()
    local lockfilePath = getLockfilePath();
    local lockfileReadHandle --[[@as CCBinaryReadHandle]], errorMessage = fs.open(lockfilePath, "rb");
    if not lockfileReadHandle then
        return { };
    end

    local lockfileContents = lockfileReadHandle.readAll();
    lockfileReadHandle.close();
    if not lockfileContents then
        return { };
    end

    local lockfileDeserialized = textutils.unserializeJSON(lockfileContents);
    return lockfileDeserialized or { };
end

---@param lockfile Lockfile
local function writeLockfile(lockfile)
    local lockfilePath = getLockfilePath();
    local lockfileWriteHandle --[[@as CCFileBinaryWriteHandle]], errorMessage = fs.open(lockfilePath, "wb");
    if not lockfileWriteHandle then
        error("Couldn't open lockfile for writing: " .. errorMessage);
    end

    local lockfileSerialized = textutils.serializeJSON(lockfile, false);

    lockfileWriteHandle.write(lockfileSerialized);
    lockfileWriteHandle.close();

    print("Updated lockfile");
end

---@return boolean
local function listContains(list, x)
    for _, v in pairs(list) do
        if v == x then return true end
    end
    return false
end

---@return boolean appliedUpdate
local function checkForSelfUpdate()
    print("ccs-get: An installer and updater")

    local updaterPath = shell.resolveProgram(arg[0]);

    if not updaterPath then
        error("Couldn't find the updater script in the program path.")
    end

    local selfFileRead --[[@as CCFileReadHandle]], selfFileReadErr = fs.open(updaterPath, "r")

    if not selfFileRead then
        error("Could not read updater script: " .. selfFileReadErr)
    end

    local httpGetUpdaterResponse --[[@as CCHttpResponse]], httpGetUpdaterErr = http.get(updaterUrl)

    if not httpGetUpdaterResponse then
        error("Could not fetch updater script: " .. httpGetUpdaterErr)
    end

    local fetchedScriptContents = httpGetUpdaterResponse.readAll() or ""
    local localScriptContents = selfFileRead.readAll()

    httpGetUpdaterResponse.close()
    selfFileRead.close()

    if fetchedScriptContents ~= localScriptContents then

        local selfFileWrite --[[@as CCFileWriteHandle]], selfFileWriteErr = fs.open(updaterPath, "w");

        if not selfFileWrite then
            error("Could not write updater script: " .. selfFileWriteErr);
        end

        selfFileWrite.write(fetchedScriptContents)
        selfFileWrite.flush()
        selfFileWrite.close()

        printColor("Updated " .. updaterProgramName .. " (" .. string.len(fetchedScriptContents) .. " chars)", colors.yellow)

        if not listContains(arg, "-ssu") then
            table.insert(arg, "-ssu")
        end

        shell.run(arg[0] .. " " .. table.concat(arg, " "))

        return true
    else
        print("No updates available for " .. updaterProgramName)
        return false
    end
end

local function updateProgramPath()
    local paths = { }

    -- separator character is a colon `:`
    for p in string.gmatch(shell.path(), "([^:]+)") do
        table.insert(paths, p)
    end

    if not listContains(paths, ccsDirectory) then
        print("Adding " .. ccsDirectory .. " to program path")
        table.insert(paths, ccsDirectory)
        local newPath = table.concat(paths, ":");
        shell.setPath(newPath);
    end
end

local function createStartupFile()
    if not fs.exists("startup.lua") then
        local startupFile --[[@as CCFileWriteHandle]], startupFileError = fs.open("startup.lua", "w")
        
        if not startupFile then
            error("Couldn't open startup file for writing: " .. startupFileError)
        end

        startupFile.writeLine("local ccs = require('ccs-get')")
        startupFile.writeLine("ccs.updateProgramPath()")
        startupFile.close()
        print("Created startup file")
    end
end

---@class GithubContent
---@field name string
---@field path string
---@field sha string
---@field size integer
---@field url string URL to a content endpoint for this file. Useful for recursing directories.
---@field download_url string URL to the file's raw content.
---@field type '"file"'|'"dir"'|'"symlink"'|'"submodule"'

---@class GithubCommitAuthor
---@field date string

---@class GithubCommitDetails
---@field author GithubCommitAuthor

---@class GithubCommit
---@field sha string
---@field commit GithubCommitDetails

local github = {
    ---Gets file and directory content from a GitHub `contents` endpoint.
    ---See https://docs.github.com/en/rest/reference/repos#get-repository-content.
    ---A valid URL will look like `https://api.github.com/repos/{owner}/{repo}/contents/{path}`.
    ---@param url? string Content endpoint to fetch
    ---@return GithubContent[] contentListing
    getContentListing = function (url)
        local headers = {
            ["Accept"] = "application/vnd.github.v3+json"
        }

        url = url or githubContentRoot

        local response --[[@as CCHttpResponse]], failureReason = http.get(url, headers)

        if not response then
            error(failureReason);
        end

        local jsonResponse = response.readAll();

        if not jsonResponse then
            error("Unexpected null JSON response from API call.")
        end

        local deserialized, deserializeErrorMessage = textutils.unserializeJSON(jsonResponse)

        if not deserialized then
            error(deserializeErrorMessage)
        end

        return deserialized
    end,

    ---Gets the latest commit from a GitHub `contents` endpoint.
    ---See https://docs.github.com/en/rest/commits/commits#list-commits
    ---A valid URL will look like `https://api.github.com/repos/{owner}/{repo}/commits`
    ---@param url? string Commit endpoint to fetch
    ---@return GithubCommit|nil
    getLatestCommit = function (url)
        local headers = {
            ["Accept"] = "application/vnd.github.v3+json"
        }

        url = url or githubCommitsRoot;

        local response --[[@as CCHttpResponse]], failureReason = http.get(url, headers);

        if not response then
            error(failureReason);
        end

        local jsonResponse = response.readAll();

        if not jsonResponse then
            error("Unexpected null JSON response from API call.");
        end

        local deserialized --[[@as table<GithubCommit>]], deserializedErrorMessage = textutils.unserializeJSON(jsonResponse);

        if not deserialized then
            error(deserializedErrorMessage);
        end

        return deserialized[1];
    end
}

---@class ContentListing
---@field localPath string
---@field githubContent GithubContent

---@param url string
---@return ContentListing[]
local function enumerateContentListings(url)
    ---@type ContentListing[]
    local contents = { }

    ---@param rUrl string
    local function recursiveGet(rUrl)

        for _, v in pairs(github.getContentListing(rUrl)) do
            if v.type == "dir" then
                recursiveGet(v.url)
            elseif v.type == "file" then
                -- Remove remote root directory (and following slash) and replace it with local root
                local remotePath = string.sub(v.path, string.len(githubScriptsDirectory) + 2, -1)
                local localDestPath = fs.combine(ccsDirectory, remotePath)
                table.insert(contents, { localPath = localDestPath, githubContent = v })
            end
        end
    end

    recursiveGet(githubContentRoot .. "/" .. githubScriptsDirectory)

    return contents
end

---@param commitHash string The full commit hash to fetch.
---@return GithubContent|nil
local function findRemoteScriptsDirectory(commitHash)
    local rootListing = github.getContentListing();

    for _, v in pairs(rootListing) do
        if v.name == githubScriptsDirectory then
            return v
        end
    end

    return nil
end

---@param commitHash string The full commit hash to fetch.
local function downloadAndWriteContentFiles(commitHash)
    local remoteScriptsContent = findRemoteScriptsDirectory(commitHash)

    if not remoteScriptsContent then
        error("Could not find '" .. githubScriptsDirectory .. "' on remote")
    else
        print("Found content directory '" .. githubScriptsDirectory .. "' on remote")
    end

    for _, v in pairs(enumerateContentListings(remoteScriptsContent.url)) do
        print("Fetching " .. v.githubContent.path)

        local destFile --[[@as CCFileBinaryWriteHandle]], destFileError = fs.open(v.localPath, "wb")

        if not destFile then
            error("Could not open file " .. v.localPath .. ": " .. destFileError)
        end

        local remoteRequest, failReason = http.get(v.githubContent.download_url, nil, true)

        if not remoteRequest then
            error(failReason)
        end

        destFile.write(remoteRequest.readAll())

        destFile.close()
        remoteRequest.close()
    end
end

local function shortHash(hash) return string.sub(hash, 1, 7) end;

---@param latestCommit GithubCommit
local function updateContent(latestCommit)
    printColor("Remote version: " .. shortHash(latestCommit.sha) .. " at " .. latestCommit.commit.author.date, colors.yellow);

    downloadAndWriteContentFiles(latestCommit.sha);

    ---@type Lockfile
    local newLockfile = {
        latestCommitSha = latestCommit.sha,
        latestCommitDate = latestCommit.commit.author.date
    };

    writeLockfile(newLockfile);
end

-- If loaded with `require`, expose some functions but do not execute main section.
if package.loaded["ccs-get"] then
    return {
        listContains = listContains,
        github = github,
        enumerateContentListings = enumerateContentListings,
        updateProgramPath = updateProgramPath,
        readLockfile = readLockfile,
        writeLockfile = writeLockfile
    }
end

-- Entry point

if not listContains(arg, "-ssu") then
    if checkForSelfUpdate() then
        return
    end
end

print("Using remote root '" .. githubContentRoot .. "'")

local latestCommit = github.getLatestCommit();

if not latestCommit then
    error("Could not fetch latest commit from remote.");
end

local oldLockfile = readLockfile();

if oldLockfile.latestCommitSha then
    print("Local version: " .. shortHash(oldLockfile.latestCommitSha) .. " at " .. oldLockfile.latestCommitDate);
end

if (listContains(arg, "-f")) then
    printColor("Forcing content update", colors.yellow);
    updateContent(latestCommit);
elseif (oldLockfile.latestCommitSha ~= latestCommit.sha) then
    printColor("Content updates available", colors.yellow);
    updateContent(latestCommit);
else
    print("No content updates available")
end

updateProgramPath();

createStartupFile();

print("Done");