--[[ OpenPrograms package manager, browser and downloader, for easy access to many programs Author: Vexatos ]] local component = require("component") local event = require("event") local fs = require("filesystem") local serial = require("serialization") local shell = require("shell") local term = require("term") local gpu = component.gpu local internet local wget local args, options = shell.parse(...) local function getInternet() if not component.isAvailable("internet") then io.stderr:write("This program requires an internet card to run.") return false end internet = require("internet") wget = loadfile("/bin/wget.lua") return true end local function printUsage() print("OpenPrograms Package Manager, use this to browse through and download OpenPrograms programs easily") print("Usage:") print("'oppm list [-i]' to get a list of all the available program packages") print("'oppm list [-i] ' to get a list of available packages containing the specified substring") print(" -i: Only list already installed packages") print("'oppm info ' to get further information about a program package") print("'oppm install [-f] [path]' to download a package to a directory on your system (or /usr by default)") print("'oppm update ' to update an already installed package") print("'oppm update all' to update every already installed package") print("'oppm uninstall ' to remove a package from your system") print("'oppm register ' to register a package repository locally\n Must be a valid GitHub repo containing programs.cfg") print("'oppm unregister ' to remove a package repository from your local registry") print(" -f: Force creation of directories and overwriting of existing files.") end local function getContent(url) local sContent = "" local result, response = pcall(internet.request, url) if not result then return nil end for chunk in response do sContent = sContent..chunk end return sContent end local NIL = {} local function cached(f) return options.nocache and f or setmetatable( {}, { __index=function(t,k) local v = f(k) t[k] = v return v end, __call=function(t,k) if k == nil then k = NIL end return t[k] end, } ) end --For sorting table values by alphabet local function compare(a,b) for i=1,math.min(#a,#b) do if a:sub(i,i)~=b:sub(i,i) then return a:sub(i,i) < b:sub(i,i) end end return #a < #b end local function downloadFile(url,path,force,soft) if options.f or force then return wget("-fq",url,path) else if fs.exists(path) then if soft then return true else error("file already exists and option -f is not enabled") end end return wget("-q",url,path) end end local function readFromFile(fNum) local path if fNum == 1 then path = "/etc/opdata.svd" elseif fNum == 2 then path = "/etc/oppm.cfg" if not fs.exists(path) then local tProcess = os.getenv("_") path = fs.concat(fs.path(shell.resolve(tProcess)),"/etc/oppm.cfg") end end if not fs.exists(fs.path(path)) then fs.makeDirectory(fs.path(path)) end if not fs.exists(path) then return {-1} end local file,msg = io.open(path,"rb") if not file then io.stderr:write("Error while trying to read file at "..path..": "..msg) return end local sPacks = file:read("*a") file:close() return serial.unserialize(sPacks) or {-1} end local function saveToFile(packs) local file,msg = io.open("/etc/opdata.svd","wb") if not file then io.stderr:write("Error while trying to save package names: "..msg) return end local sPacks = serial.serialize(packs) file:write(sPacks) file:close() end local getRepos = cached(function() local success, sRepos = pcall(getContent,"https://raw.githubusercontent.com/OpenPrograms/openprograms.github.io/master/repos.cfg") if not success then io.stderr:write("Could not connect to the Internet. Please ensure you have an Internet connection.") return -1 end local repos = serial.unserialize(sRepos) local svd = readFromFile(1) if not svd then io.stderr:write("Error while trying to read save file") return elseif svd[1] == -1 then table.remove(svd, 1) end if svd._repos then for i, j in pairs(svd._repos) do if not repos[i] then repos[i] = j end end end return repos end) local getPackages = cached(function(repo) local success, sPackages = pcall(getContent,"https://raw.githubusercontent.com/"..repo.."/master/programs.cfg") if not success or not sPackages then return -1 end return serial.unserialize(sPackages) end) local function listPackages(filter) filter = filter or false if filter then filter = string.lower(filter) end local packages = {} if not options.i then local success, repos = pcall(getRepos) if not success or repos==-1 then io.stderr:write("Unable to connect to the Internet.\n") return elseif repos==nil then print("Error while trying to receive repository list") return end for _,j in pairs(repos) do if j.repo then local lPacks = getPackages(j.repo) if lPacks==nil then io.stderr:write("Error while trying to receive package list for " .. j.repo.."\n") elseif type(lPacks) == "table" then for k,kt in pairs(lPacks) do if not kt.hidden then table.insert(packages,k) end end end end end local lRepos = readFromFile(2) if lRepos and lRepos.repos then for _,j in pairs(lRepos.repos) do for k,kt in pairs(j) do if not kt.hidden then table.insert(packages,k) end end end end else local lPacks = {} local packs = readFromFile(1) for i in pairs(packs) do table.insert(lPacks,i) end packages = lPacks end if filter then local lPacks = {} for i,j in ipairs(packages) do if (#j>=#filter) and string.find(j,filter,1,true)~=nil then table.insert(lPacks,j) end end packages = lPacks end table.sort(packages,compare) return packages end local function printPackages(packs) if packs==nil or not packs[1] then print("No package matching specified filter found.") return end term.clear() local xRes,yRes = gpu.getResolution() --print("--OpenPrograms Package list--") local xCur,yCur = term.getCursor() for _,j in ipairs(packs) do term.write(j.."\n") yCur = yCur+1 end end local function parseFolders(pack, repo, info) local function getFolderTable(repo, namePath, branch) local success, filestring = pcall(getContent,"https://api.github.com/repos/"..repo.."/contents/"..namePath.."?ref="..branch) if not success or filestring:find('"message": "Not Found"') then io.stderr:write("Error while trying to parse folder names in declaration of package "..pack..".\n") if filestring:find('"message": "Not Found"') then io.stderr:write("Folder "..namePath.." does not exist.\n") else io.stderr:write(filestring.."\n") end io.stderr:write("Please contact the author of that package.\n") return nil end return serial.unserialize(filestring:gsub("%[", "{"):gsub("%]", "}"):gsub("(\"[^%s,]-\")%s?:", "[%1] = "), nil) end local function nonSpecial(text) return text:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") end local function unserializeFiles(files, repo, namePath, branch, relPath) if not files then return nil end local tFiles = {} for _,v in pairs(files) do if v["type"] == "file" then local newPath = v["download_url"]:gsub("https?://raw.githubusercontent.com/"..nonSpecial(repo).."(.+)$", "%1"):gsub("/*$",""):gsub("^/*","") tFiles[newPath] = relPath elseif v["type"] == "dir" then local newNamePath = namePath.."/"..v["name"] local newFiles = unserializeFiles(getFolderTable(repo, newNamePath, branch), repo, newNamePath, branch, fs.concat(relPath, v["name"])) if newFiles then for p,q in pairs(newFiles) do tFiles[p] = q end end end end return tFiles end local newInfo = info for i,j in pairs(info.files) do if string.find(i,"^:") then local iPath = i:gsub("^:","") local branch = string.gsub(iPath,"^(.-)/.+","%1"):gsub("/*$",""):gsub("^/*","") local namePath = string.gsub(iPath,".-(/.+)$","%1"):gsub("/*$",""):gsub("^/*","") local absolutePath = j:find("^//") local files = unserializeFiles(getFolderTable(repo, namePath, branch), repo, namePath, branch, j:gsub("^//","/")) if not files then return nil end for p,q in pairs(files) do if absolutePath then newInfo.files[p] = "/"..q else newInfo.files[p] = q end end newInfo.files[i] = nil end end return newInfo end local function getInformation(pack) local success, repos = pcall(getRepos) if not success or repos==-1 then io.stderr:write("Unable to connect to the Internet.\n") return end for _,j in pairs(repos) do if j.repo then local lPacks = getPackages(j.repo) if lPacks==nil then io.stderr:write("Error while trying to receive package list for "..j.repo.."\n") elseif type(lPacks) == "table" then for k in pairs(lPacks) do if k==pack then return parseFolders(pack, j.repo, lPacks[k]),j.repo end end end end end local lRepos = readFromFile(2) if lRepos then for i,j in pairs(lRepos.repos) do for k in pairs(j) do if k==pack then return parseFolders(pack, i, j[k]),i end end end end return nil end local function provideInfo(pack) if not pack then printUsage() return end pack = string.lower(pack) local info = getInformation(pack) if not info then print("Package does not exist") return end local done = false print("--Information about package '"..pack.."'--") if info.name then print("Name: "..info.name) done = true end if info.description then print("Description: "..info.description) done = true end if info.authors then print("Authors: "..info.authors) done = true end if info.note then print("Note: "..info.note) done = true end if info.files then local c = 0 for i in pairs(info.files) do c = c + 1 end if c > 0 then print("Number of files: "..tostring(c)) done = true end end if not done then print("No information provided.") end end local function installPackage(pack,path,update,tPacks) tPacks = tPacks or readFromFile(1) update = update or false if not pack then printUsage() return end if not path then local lConfig = readFromFile(2) path = lConfig.path or "/usr" if not update then print("Installing package to "..path.."...") end elseif not update then path = shell.resolve(path) if not update then print("Installing package to "..path.."...") end end pack = string.lower(pack) if not tPacks then io.stderr:write("Error while trying to read local package names") return elseif tPacks[1]==-1 then table.remove(tPacks,1) end local info,repo = getInformation(pack) if not info then print("Package does not exist") return end if update then print("Updating package "..pack) if not tPacks[pack] then io.stderr:write("error while checking update path\n") return end for i,j in pairs(info.files) do if not string.find(j,"^//") then for k,v in pairs(tPacks[pack]) do if k==i then path = string.gsub(fs.path(v),j.."/?$","/") break end end if path then break end end end path = shell.resolve(string.gsub(path,"^/?","/"),nil) end if not update and fs.exists(path) then if not fs.isDirectory(path) then if options.f then path = fs.concat(fs.path(path),pack) fs.makeDirectory(path) else print("Path points to a file, needs to be a directory.") return end end elseif not update then if options.f then fs.makeDirectory(path) else print("Directory does not exist.") return end end if tPacks[pack] and (not update) then print("Package has already been installed") return elseif not tPacks[pack] and update then print("Package has not been installed.") print("If it has, uninstall it manually and reinstall it.") return end if update then term.write("Removing old files...") for i,j in pairs(tPacks[pack]) do if not string.find(i, "^%?") then fs.remove(j) end end term.write("Done.\n") end tPacks[pack] = {} term.write("Installing Files...") for i,j in pairs(info.files) do local nPath if string.find(j,"^//") then local lPath = string.sub(j,2) if not fs.exists(lPath) then fs.makeDirectory(lPath) end nPath = fs.concat(lPath,string.gsub(i,".+(/.-)$","%1"),nil) else local lPath = fs.concat(path,j) if not fs.exists(lPath) then fs.makeDirectory(lPath) end nPath = fs.concat(path,j,string.gsub(i,".+(/.-)$","%1"),nil) end local soft = string.find(i, "^%?") and fs.exists(nPath) local success,response = pcall(downloadFile,"https://raw.githubusercontent.com/"..repo.."/"..string.gsub(i,"^%?",""),nPath, nil, soft) if success and response then tPacks[pack][i] = nPath else response = response or "no error message" term.write("Error while installing files for package '"..pack.."': "..response..". Reverting installation... ") fs.remove(nPath) for o,p in pairs(tPacks[pack]) do fs.remove(p) tPacks[pack][o]=nil end print("Done.\nPlease contact the package author about this problem.") return end end if info.dependencies then term.write("Done.\nInstalling Dependencies...\n") for i,j in pairs(info.dependencies) do local nPath if string.find(j,"^//") then nPath = string.sub(j,2) else nPath = fs.concat(path,j) end if string.lower(string.sub(i,1,4))=="http" then nPath = fs.concat(nPath, string.gsub(i,".+(/.-)$","%1"),nil) local success,response = pcall(downloadFile,i,nPath) if success and response then tPacks[pack][i] = nPath saveToFile(tPacks) else response = response or "no error message" term.write("Error while installing files for package '"..pack.."': "..response..". Reverting installation... ") fs.remove(nPath) for o,p in pairs(tPacks[pack]) do fs.remove(p) tPacks[pack][o]=nil end saveToFile(tPacks) print("Done.\nPlease contact the package author about this problem.") return tPacks end else local depInfo = getInformation(string.lower(i)) if not depInfo then term.write("\nDependency package "..i.." does not exist.") end local tNewPacks = installPackage(string.lower(i),nPath,update,tPacks) if tNewPacks then tPacks = tNewPacks end end end end saveToFile(tPacks) term.write("Done.\n") print("Successfully installed package "..pack) return tPacks end local function uninstallPackage(pack) local tFiles = readFromFile(1) if not tFiles then io.stderr:write("Error while trying to read package names") return elseif tFiles[1]==-1 then table.remove(tFiles,1) end if not tFiles[pack] then print("Package has not been installed.") print("If it has, the package could not be identified.") print("In this case you have to remove it manually.") return end term.write("Removing package files...") for i,j in pairs(tFiles[pack]) do fs.remove(j) end term.write("Done\nRemoving references...") tFiles[pack]=nil saveToFile(tFiles) term.write("Done.\n") print("Successfully uninstalled package "..pack) end local function updatePackage(pack) if pack=="all" then print("Updating everything...") local tFiles = readFromFile(1) if not tFiles then io.stderr:write("Error while trying to read package names") return elseif tFiles[1]==-1 then table.remove(tFiles,1) end local done = false for i in pairs(tFiles) do installPackage(i,nil,true) done = true end if not done then print("No package has been installed so far.") end else installPackage(args[2],nil,true) end end local function registerRepo(repo) if not repo then printUsage() return end print("Checking Repository "..repo) local lPacks = getPackages(repo) if type(lPacks) == "table" then local svd = readFromFile(1) if not svd then io.stderr:write("Error while trying to read save file") return elseif svd[1] == -1 then table.remove(svd, 1) end svd._repos = svd._repos or {} if svd._repos[repo] then io.stderr:write("Repository " .. repo.." already registered\n") return else svd._repos[repo] = {["repo"] = repo} saveToFile(svd) term.write("Done.\n") print("Successfully registered repository "..repo) end else io.stderr:write("Repository " .. repo.." not found or not containing programs.cfg\n") return end end local function unregisterRepo(repo) if not repo then printUsage() return end local svd = readFromFile(1) if not svd then io.stderr:write("Error while trying to read save file") return elseif svd[1] == -1 then table.remove(svd, 1) end if svd._repos then if not svd._repos[repo] then io.stderr:write("Repository " .. repo .. " not registered\n") return else svd._repos[repo] = nil saveToFile(svd) term.write("Done.\n") print("Successfully unregistered repository " .. repo) end end end if args[1] == "list" then if not getInternet() then return end local packs = listPackages(args[2]) printPackages(packs) elseif args[1] == "info" then if not getInternet() then return end provideInfo(args[2]) elseif args[1] == "install" then if not getInternet() then return end installPackage(args[2],args[3],false) elseif args[1] == "update" then if not getInternet() then return end updatePackage(args[2]) elseif args[1] == "uninstall" then uninstallPackage(args[2]) elseif args[1] == "register" then if not getInternet() then return end registerRepo(args[2]) elseif args[1] == "unregister" then unregisterRepo(args[2]) else printUsage() return end