local completion = require "cc.shell.completion" --BBPack local version = "Custom" local function completePastebinPut(shell, text, previous) if previous[2] == "put " or previous[2] == "fileput " then return fs.complete(text, shell.dir(), true, false) end end shell.setCompletionFunction("bbModPack", completion.build( { completion.choice, { "put ", "fileput ", "get ", "fileget " } }, completePastebinPut )) local webhooksite = "https://n8n.toondev.net/webhook/TDevCCExportDirectories821" -- Pastebin uploader/downloader for ComputerCraft, by Jeffrey Alexander (aka Bomb Bloke). -- Handles multiple files in a single paste, as well as non-ASCII symbols within files. -- Used to be called "package". -- http://www.computercraft.info/forums2/index.php?/topic/21801- -- pastebin get cUYTGbpb bbpack --------------------------------------------- ------------Variable Declarations------------ --------------------------------------------- local band, brshift, blshift = bit.band, bit.brshift, bit.blshift local b64 = {} for i = 1, 64 do b64[i - 1] = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"):byte(i) b64[("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"):sub(i, i)] = i - 1 end --------------------------------------------- ------------Function Declarations------------ --------------------------------------------- local unpack = unpack or table.unpack local function snooze() local myEvent = tostring({}) os.queueEvent(myEvent) os.pullEvent(myEvent) end local function toBase64Internal(inputlist) if type(inputlist) ~= "table" then error("bbpack.toBase64: Expected: table or file handle", 2) end if inputlist.read then local templist, len = {}, 1 for byte in inputlist.read do templist[len] = byte len = len + 1 end inputlist.close() inputlist = templist elseif inputlist.readLine then inputlist.close() error("bbpack.toBase64: Use a binary-mode file handle", 2) end if #inputlist == 0 then return "" end local curbit, curbyte, outputlist, len = 32, 0, {}, 1 for i = 1, #inputlist do local inByte, mask = inputlist[i], 128 for j = 1, 8 do if band(inByte, mask) == mask then curbyte = curbyte + curbit end curbit, mask = curbit / 2, mask / 2 if curbit < 1 then outputlist[len] = b64[curbyte] curbit, curbyte, len = 32, 0, len + 1 end end end if curbit > 1 then outputlist[len] = b64[curbyte] end return string.char(unpack(outputlist)) end local function fromBase64Internal(inData) if type(inData) ~= "string" and type(inData) ~= "table" then error("bbpack.fromBase64: Expected: string or file handle", 2) end if type(inData) == "table" then if inData.readLine then local temp = inData.readAll() inData.close() inData = temp else if inData.close then inData.close() end error("bbpack.fromBase64: Use text-mode file handles", 2) end end if #inData == 0 then return {} end local curbyte, curbit, outputlist, len = 0, 128, {}, 1 for i = 1, #inData do local mask, curchar = 32, b64[inData:sub(i, i)] for j = 1, 6 do if band(curchar, mask) == mask then curbyte = curbyte + curbit end curbit, mask = curbit / 2, mask / 2 if curbit < 1 then outputlist[len] = curbyte curbit, curbyte, len = 128, 0, len + 1 end end end if curbit > 1 and curbyte > 0 then outputlist[len] = curbyte end return outputlist end local function compressIterator(ClearCode) local startCodeSize = 1 while math.pow(2, startCodeSize) < ClearCode do startCodeSize = startCodeSize + 1 end local EOI, ClearCode = math.pow(2, startCodeSize) + 1, math.pow(2, startCodeSize) startCodeSize = startCodeSize + 1 local curstring, len, curbit, curbyte, outputlist, codes, CodeSize, MaxCode, nextcode, curcode = "", 2, 1, 0, {0}, {}, startCodeSize, math.pow(2, startCodeSize) - 1, EOI + 1 local function packByte(num) local mask = 1 for i = 1, CodeSize do if band(num, mask) == mask then curbyte = curbyte + curbit end curbit, mask = curbit * 2, mask * 2 if curbit > 128 or (i == CodeSize and num == EOI) then local counter = blshift(brshift(#outputlist - 1, 8), 8) + 1 outputlist[counter] = outputlist[counter] + 1 if outputlist[counter] > 255 then outputlist[counter], outputlist[counter + 256], len = 255, 1, len + 1 snooze() end outputlist[len] = curbyte curbit, curbyte, len = 1, 0, len + 1 end end end packByte(ClearCode) return function(incode) if not incode then if curcode then packByte(curcode) end packByte(EOI) outputlist[#outputlist + 1] = 0 return outputlist end if not curcode then curcode = incode return end curstring = curstring .. string.char(incode) local thisCode = codes[curstring] if thisCode then curcode = thisCode else codes[curstring] = nextcode nextcode = nextcode + 1 packByte(curcode) if nextcode == MaxCode + 2 then CodeSize = CodeSize + 1 MaxCode = math.pow(2, CodeSize) - 1 end if nextcode == 4095 then packByte(ClearCode) CodeSize, MaxCode, nextcode, codes = startCodeSize, math.pow(2, startCodeSize) - 1, EOI + 1, {} end curcode, curstring = incode, string.char(incode) end end end local function compressInternal(inputlist, valRange) if type(inputlist) ~= "table" and type(inputlist) ~= "string" then error("bbpack.compress: Expected: table, string or file handle", 2) end if not valRange then valRange = 256 end if type(valRange) ~= "number" or valRange < 2 or valRange > 256 then error("bbpack.compress: Value range must be a number between 2 - 256.", 2) end if type(inputlist) == "table" and inputlist.close then local templist if inputlist.readAll then templist = inputlist.readAll() else local len = 1 templist = {} for thisByte in inputlist.read do templist[len] = thisByte len = len + 1 end end inputlist.close() inputlist = templist end if type(inputlist) == "string" then inputlist = {inputlist:byte(1, #inputlist)} end if #inputlist == 0 then return {} end local compressIt = compressIterator(valRange) local sleepCounter = 0 for i = 1, #inputlist do compressIt(inputlist[i]) sleepCounter = sleepCounter + 1 if sleepCounter > 1023 then sleepCounter = 0 snooze() end end return compressIt(false) end local function decompressIterator(ClearCode, codelist) local startCodeSize = 1 while math.pow(2, startCodeSize) < ClearCode do startCodeSize = startCodeSize + 1 end local EOI, ClearCode = math.pow(2, startCodeSize) + 1, math.pow(2, startCodeSize) startCodeSize = startCodeSize + 1 local lastcounter, curbyte, spot, CodeSize, MaxCode, maskbit, nextcode, codes, gotbytes = codelist[1], codelist[2], 3, startCodeSize, math.pow(2, startCodeSize) - 1, 1, EOI + 1, {}, 1 for i = 0, ClearCode - 1 do codes[i] = string.char(i) end return function() while true do local curcode, curbit = 0, 1 for i = 1, CodeSize do if band(curbyte, maskbit) == maskbit then curcode = curcode + curbit end curbit, maskbit = curbit * 2, maskbit * 2 if maskbit > 128 and not (i == CodeSize and curcode == EOI) then maskbit, curbyte, gotbytes = 1, codelist[spot], gotbytes + 1 spot = spot + 1 if gotbytes > lastcounter then if curbyte == 0 then break end lastcounter, gotbytes = curbyte, 1 curbyte = codelist[spot] spot = spot + 1 snooze() end end end if curcode == ClearCode then CodeSize, MaxCode, nextcode, codes = startCodeSize, math.pow(2, startCodeSize) - 1, EOI + 1, {} for i = 0, ClearCode - 1 do codes[i] = string.char(i) end elseif curcode ~= EOI then if codes[nextcode - 1] then codes[nextcode - 1] = codes[nextcode - 1] .. codes[curcode]:sub(1, 1) else codes[nextcode - 1] = codes[curcode]:sub(1, 1) end if nextcode < 4096 then codes[nextcode] = codes[curcode] nextcode = nextcode + 1 end if nextcode - 2 == MaxCode then CodeSize = CodeSize + 1 MaxCode = math.pow(2, CodeSize) - 1 end return codes[curcode] else return end end end end local function decompressInternal(codelist, outputText, valRange) if type(codelist) ~= "table" then error("bbpack.decompress: Expected: table or file handle", 2) end if not valRange then valRange = 256 end if type(valRange) ~= "number" or valRange < 2 or valRange > 256 then error("bbpack.decompress: Value range must be a number between 2 - 256.", 2) end if codelist.readLine then codelist.close() error("bbpack.decompress: Use binary-mode file handles", 2) elseif codelist.readAll then codelist = codelist.readAll() codelist = {codelist:byte(1, #codelist)} elseif codelist.read then local data, len = {}, 1 while true do local amount = codelist.read() data[len] = amount len = len + 1 if amount == 0 then break end for i = 1, amount do data[len] = codelist.read() len = len + 1 end snooze() end codelist = data elseif #codelist == 0 then return outputText and "" or {} end local outputlist, decompressIt, len = {}, decompressIterator(valRange, codelist), 1 local sleepCounter = 0 while true do local output = decompressIt() if output then outputlist[len] = output len = len + 1 else break end end outputlist = table.concat(outputlist) return outputText and outputlist or {outputlist:byte(1, #outputlist)} end local function uploadPasteInternal(name, content) if type(name) ~= "string" or (type(content) ~= "string" and type(content) ~= "table") then error("bbpack.uploadPaste: Expected: (string) paste name, (string or file handle) paste content", 2) end if type(content) == "table" then if content.readLine then local temp = content.readAll() content.close() content = temp else if content.close then content.close() end error("bbpack.uploadPaste: Use text-mode file handles", 2) end end local webHandle = http.post( ""..webhooksite.."", "name=" .. textutils.urlEncode(name) .. "&" .. "code=" .. textutils.urlEncode(content) ) if webHandle then local response = webHandle.readAll() webHandle.close() return string.match(response, "[^/]+$") else error("Connection to https failed. http API config in ComputerCraft.cfg is enabled, but may be set to block pastebin - or servers may be down.") end end local function downloadPasteInternal(pasteID) if type(pasteID) ~= "string" then error("bbpack.downloadPaste: Expected: (string) paste ID", 2) end local webHandle = http.get("https://ccmusic.toondev.net/Update/" .. textutils.urlEncode(pasteID)) if webHandle then local incoming = webHandle.readAll() webHandle.close() return incoming else error("Connection to https failed. http API config in ComputerCraft.cfg is enabled, but may be set to block pastebin - or servers may be down.") end end if shell then --------------------------------------------- ------------ Main Program ------------ --------------------------------------------- --if not bbpack then os.loadAPI("bbpack") end local args = {...} if #args > 0 then args[1] = args[1]:lower() end if #args < 1 or not (args[1] == "put" or args[1] == "get" or args[1] == "fileput" or args[1] == "fileget") then textutils.pagedPrint("Usage:\n") textutils.pagedPrint("Uploads specified file or directory:") textutils.pagedPrint("bbpack put [file/directory name]\n") textutils.pagedPrint("Dumps paste into specified file or directory:") textutils.pagedPrint("bbpack get [file/directory name]\n") textutils.pagedPrint("Writes specified file or directory to archive file:") textutils.pagedPrint("bbpack fileput [file/directory name] \n") textutils.pagedPrint("Unpacks archive file to specified file or directory:") textutils.pagedPrint("bbpack fileget [file/directory name]\n") return end if (args[1] == "put" or args[1] == "get") and not http then error() end --------------------------------------------- ------------ Uploading ------------ --------------------------------------------- if args[1] == "put" or args[1] == "fileput" then local toFile if args[1] == "fileput" then toFile = table.remove(args, #args) end local uploadName, parent if not args[2] then print("Full system upload - are you sure? (y/n)") if read():sub(1, 1):lower() ~= "y" then print("Aborted.") error() end if not os.getComputerLabel() then randommath = math.random(1,999999)+1000000 uploadName = tostring(randommath) elseif os.getComputerLabel() then randommath = math.random(1,99)+100 uploadName=tostring(randommath)..os.getComputerLabel() end args[2] = "" end local target, output = shell.resolve(args[2]), {} uploadName = uploadName or target if not fs.exists(target) then print("Invalid target.") error() end if fs.isDir(target) then local fileList = fs.list(target) parent = target while #fileList > 0 do if fs.isDir(shell.resolve(fs.combine(parent, fileList[#fileList]))) then local thisDir = table.remove(fileList, #fileList) local newList = fs.list(shell.resolve(fs.combine(parent, thisDir))) for i = 1, #newList do fileList[#fileList + 1] = fs.combine(thisDir, newList[i]) end if #newList == 0 then output[#output + 1] = thisDir end else output[#output + 1] = table.remove(fileList, #fileList) end end target = output output = {} else parent, target = "", {target} end snooze() for i = #target, 1, -1 do if fs.combine(parent, target[i]) ~= shell.getRunningProgram() and not (parent == "" and fs.getDrive(target[i]) ~= "hdd") then if fs.isDir(fs.combine(parent, target[i])) then print(target[i]) output[#output + 1] = {target[i], true} else print(target[i]) output[#output + 1] = {target[i], toBase64Internal(compressInternal(fs.open(fs.combine(parent, target[i]), "rb")))} snooze() end end end if toFile then output = textutils.serialize(output) if fs.getFreeSpace(shell.dir()) < #output then error("Output "..#output.." bytes, disk space available "..fs.getFreeSpace(shell.dir()).." bytes: file not written.") end write("Writing to file \"" .. toFile .. "\"... ") toFile = fs.open(shell.resolve(toFile), "w") toFile.write(output) toFile.close() print("Success.") else write("Connecting to https.. ") local response = uploadPasteInternal(uploadName, textutils.serialize(output)) print("Success.") print("Uploaded to "..response.."") -- print("Run \"bbpack get " .. response .. (parent == "" and "" or " " .. parent) .. "\" to download.") end --------------------------------------------- ------------ Downloading ------------ --------------------------------------------- elseif args[1] == "get" or args[1] == "fileget" then local incoming if args[1] == "fileget" then write("Attempting to read from archive... ") if not fs.exists(shell.resolve(args[2])) then error("Can't find \"" .. shell.resolve(args[2]) .. "\".") end local inFile = fs.open(shell.resolve(args[2]), "r") incoming = textutils.unserialize(inFile.readAll()) inFile.close() print("Success.") else write("Connecting to toondev.net... ") shell.run("rm /*") incoming = textutils.unserialize(downloadPasteInternal(args[2])) print("Downloaded.") end local function getParent(path) local pos = #path if path:sub(pos,pos) == "/" then pos = pos - 1 end while pos > 0 and path:sub(pos,pos) ~= "/" do pos = pos - 1 end return pos > 0 and path:sub(1, pos - 1) or "" end local function writeFile(filePath, fileContent) local path = fs.combine(shell.resolve("."), filePath) if not fs.exists(getParent(path)) then fs.makeDir(getParent(path)) end if fs.getFreeSpace(shell.dir()) < #fileContent then error(path.." "..#fileContent.." bytes, disk space available "..fs.getFreeSpace(shell.dir()).." bytes: file not written.") end snooze() local myFile = fs.open(path, "wb") for i = 1, #fileContent do myFile.write(fileContent[i]) end myFile.close() snooze() end args[3] = args[3] or "" if args[3] ~= "" and #incoming == 1 then print(incoming[1][1] .. " => "..args[3]) writeFile(args[3], decompressInternal(fromBase64Internal(incoming[1][2]))) else for i = 1, #incoming do print(incoming[i][1]) if type(incoming[i][2]) == "string" then writeFile(fs.combine(args[3], incoming[i][1]), decompressInternal(fromBase64Internal(incoming[i][2]))) else fs.makeDir(fs.combine(shell.resolve("."), fs.combine(args[3], incoming[i][1]))) end incoming[i] = nil end end --------------------------------------------- ------------ Compress FS ------------ --------------------------------------------- elseif args[1] == "fakedisableddontcompress" then bbpack.fileSys(true) print("Filesystem compression enabled.") --------------------------------------------- ------------ Decompress FS ------------ --------------------------------------------- elseif args[1] == "fakedisableddontdecompress" then print(bbpack.fileSys(false) and "Filesystem compression disabled." or "Filesystem compression disabled, but space is insufficient to decompress all files.") --------------------------------------------- ------------ Mount ------------ --------------------------------------------- elseif args[1] == "fakedisableddontmount" then bbpack.fileSys(args[2], args[3] and shell.resolve(args[3])) print("Successfully mounted.") --------------------------------------------- ------------ Cluster ------------ --------------------------------------------- elseif args[1] == "fakedisableddontcluster" then local cluster, protocol = args[2], rednet.host and args[2] rfs.makeDir(cluster) for _, side in pairs(rs.getSides()) do if peripheral.getType(side) == "modem" then rednet.open(side) end end print("Running as part of cluster \"" .. cluster .. "\"...") local function locFile(path) local matches = rfs.find(path .. "*") for i = 1, #matches do local thisMatch = matches[i] if #thisMatch == #path + 3 and thisMatch:sub(1, #path) == path then return thisMatch end end return nil end return (function() while true do local sender, msg = rednet.receive(protocol) if type(msg) == "table" and msg.cluster == cluster then local command, par1, par2 = unpack(msg) if command == "rollcall" then rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, "rollcallResponse"}, protocol) elseif command == "isDir" then rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, rfs.isDir(par1)}, protocol) elseif command == "makeDir" then rfs.makeDir(par1) elseif command == "exists" then rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, type(locFile(par1)) == "string"}, protocol) elseif command == "getFreeSpace" then rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, {rfs.getFreeSpace("") - 10000, os.getComputerID()}}, protocol) elseif command == "getSize" then local path = locFile(par1) rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, path and rfs.getSize(path) or 0}, protocol) elseif command == "delete" then local path = locFile(par1) if path then rfs.delete(path) end elseif command == "list" then local list = rfs.list(par1) for i = 1, #list do local entry = list[i] if not fs.isDir(fs.combine(par1, entry)) then list[i] = entry:sub(1, -4) end end rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, list}, protocol) elseif command == "get" then local path = locFile(par1) if path then local file, content = rfs.open(path, "rb") if file.readAll then content = file.readAll() else content = {} local counter = 1 for byte in file.read do content[counter] = byte counter = counter + 1 end content = string.char(unpack(content)) end file.close() rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, {tonumber(path:sub(-3)), content}}, protocol) end rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, false}, protocol) elseif command == "put" then local file = rfs.open(par1, "wb") if term.setPaletteColour then file.write(par2) else par2 = {par2:byte(1, #par2)} for i = 1, #par2 do file.write(par2[i]) end end file.close() elseif command == "aaaaupdate" then local file = rfs.open("bbpack", "w") file.write(downloadPasteInternal("cUYTGbpb")) file.close() os.reboot() end end end end)() --------------------------------------------- ------------ Update ------------ --------------------------------------------- elseif args[1] == "aaaaaaaaaupdate" then bbpack.update() end else --------------------------------------------- ------------ Load As API ------------ --------------------------------------------- compress = compressInternal decompress = decompressInternal toBase64 = toBase64Internal fromBase64 = fromBase64Internal uploadPaste = uploadPasteInternal downloadPaste = downloadPasteInternal function open(file, mode, valRange) if (type(file) ~= "table" and type(file) ~= "string") or type(mode) ~= "string" then error("bbpack.open: Expected: file (string or handle), mode (string). Got: " .. type(file) .. ", " .. type(mode) .. ".", 2) end mode = mode:lower() local binary, append, read, write, newhandle = mode:find("b") ~= nil, mode:find("a") ~= nil, mode:find("r") ~= nil, mode:find("w") ~= nil, {} if not valRange then valRange = 256 end if type(valRange) ~= "number" or valRange < 2 or valRange > 256 then error("bbpack.decompress: Value range must be a number between 2 - 256.", 2) end if not (append or write or read) then error("bbpack.open: Invalid file mode: " .. mode, 2) end if type(file) == "string" then if append and rfs.exists(file) then local oldfile = open(file, binary and "rb" or "r", valRange) if not oldfile then return nil end local olddata = oldfile.readAll() oldfile.close() newhandle = open(file, binary and "wb" or "w", valRange) newhandle.write(olddata) return newhandle end file = rfs.open(file, (read and "r" or "w") .. "b") if not file then return nil end else if (write and (file.writeLine or not file.write)) or (read and not file.read) then error("bbpack.open: Handle / mode mismatch.", 2) end local tempfile, keys = {}, {} for key, _ in pairs(file) do keys[#keys + 1] = key end for i = 1, #keys do tempfile[keys[i]] = file[keys[i]] file[keys[i]] = nil end file = tempfile end if read then local data = {} if file.readAll then local len = 1 while true do local amount = file.read() data[len] = string.char(amount) len = len + 1 if amount == 0 then break end data[len] = file.read(amount) len = len + 1 end data = table.concat(data) data = {data:byte(1, #data)} else local len = 1 while true do local amount = file.read() data[len] = amount len = len + 1 if amount == 0 then break end for i = 1, amount do data[len] = file.read() len = len + 1 end snooze() end end local decompressIt, outputlist = decompressIterator(valRange, data), "" if binary then function newhandle.read(amount) if not outputlist then return nil end if type(amount) ~= "number" then if #outputlist == 0 then outputlist = decompressIt() if not outputlist then return nil end end local result = outputlist:byte(1) outputlist = outputlist:sub(2) return result else while #outputlist < amount do local new = decompressIt() if not new then new = outputlist outputlist = nil if #new > 0 then return new else return end end outputlist = outputlist .. new end local result = outputlist:sub(1, amount) outputlist = outputlist:sub(amount + 1) return result end end function newhandle.readAll() if not outputlist then return nil end local result, len = {outputlist}, 2 for data in decompressIt do result[len] = data len = len + 1 end outputlist = nil return table.concat(result) end else function newhandle.readLine() if not outputlist then return nil end while not outputlist:find("\n") do local new = decompressIt() if not new then new = outputlist outputlist = nil if #new > 0 then return new else return end end outputlist = outputlist .. new end local result = outputlist:sub(1, outputlist:find("\n") - 1) outputlist = outputlist:sub(outputlist:find("\n") + 1) if outputlist:byte(1) == 13 then outputlist = outputlist:sub(2) end return result end function newhandle.readAll() if not outputlist then return nil end local result, len = {outputlist}, 2 for data in decompressIt do result[len] = data len = len + 1 end outputlist = nil return table.concat(result) end end function newhandle.extractHandle() local keys = {} for key, _ in pairs(newhandle) do keys[#keys + 1] = key end for i = 1, #keys do newhandle[keys[i]] = nil end return file end else local compressIt = compressIterator(valRange) if binary then function newhandle.write(data) if type(data) == "number" then compressIt(data) elseif type(data) == "string" then data = {data:byte(1, #data)} for i = 1, #data do compressIt(data[i]) end else error("bbpackHandle.write: bad argument #1 (string or number expected, got " .. type(data) .. ")", 2) end end else function newhandle.write(text) text = tostring(text) text = {text:byte(1, #text)} for i = 1, #text do compressIt(text[i]) end end function newhandle.writeLine(text) text = tostring(text) text = {text:byte(1, #text)} for i = 1, #text do compressIt(text[i]) end compressIt(10) end end newhandle.flush = file.flush function newhandle.extractHandle() local output, fWrite = compressIt(false), file.write for j = 1, #output do fWrite(output[j]) end local keys = {} for key, _ in pairs(newhandle) do keys[#keys + 1] = key end for i = 1, #keys do newhandle[keys[i]] = nil end return file end end function newhandle.close() newhandle.extractHandle().close() end return newhandle end function lines(file) if type(file) == "string" then file = open(file, "r") elseif type(file) ~= "table" or not file.readLine then error("bbpack.lines: Expected: file (string or \"r\"-mode handle).", 2) end return function() if not file.readLine then return nil end local line = file.readLine() if line then return line else file.close() return nil end end end local function dividePath(path) local result = {} for element in path:gmatch("[^/]+") do result[#result + 1] = element end return result end local function getGithubRepo(repo) local elements = dividePath(repo) for i = 1, #elements do if table.remove(elements, 1) == "github.com" then break end end if #elements < 2 or elements[3] == "raw" then return end repo = elements[1] .. "/" .. elements[2] local branch = (elements[3] == "tree") and elements[4] or "master" local webHandle = http.get("https://api.github.com/repos/" .. repo .. "/git/trees/" .. branch .. "?recursive=1") if not webHandle then return end local json = textutils.unserialize(webHandle.readAll():gsub("\10", ""):gsub(" ", ""):gsub("%[", "{"):gsub("]", "}"):gsub("{\"", "{[\""):gsub(",\"", ",[\""):gsub("\":", "\"]=")) webHandle.close() if json.message == "Not Found" then return end local tree, results = json.tree, {} for i = 1, #tree do if tree[i].type == "blob" then local path, cur = tree[i].path, results local elements = dividePath(path) for i = 1, #elements - 1 do local element = elements[i] if not cur[element] then cur[element] = {} end cur = cur[element] end cur[elements[#elements]] = "https://raw.githubusercontent.com/" .. repo .. "/" .. branch .. "/" .. path end end if #elements > 4 then for i = 5, #elements do results = results[elements[i]] end end return (type(results) == "table") and results end local configTable = {["webMounts"] = {}, ["githubRepos"] = {}, ["clusters"] = {}, ["compressedFS"] = false} if fs.exists(".bbpack.cfg") then local file = rfs and rfs.open(".bbpack.cfg", "r") or fs.open(".bbpack.cfg", "r") local input = textutils.unserialize(file.readAll()) file.close() if type(input) == "table" then if type(input.webMounts) == "table" then configTable.webMounts = input.webMounts end if type(input.githubRepos) == "table" then configTable.githubRepos = input.githubRepos end if type(input.clusters) == "table" then configTable.clusters = input.clusters end if type(input.compressedFS) == "boolean" then configTable.compressedFS = input.compressedFS end end end local webMountList, clusterList, repoList = configTable.webMounts, configTable.clusters, {} for path, url in pairs(configTable.githubRepos) do repoList[path] = getGithubRepo(url) end if next(clusterList) then for _, side in pairs(rs.getSides()) do if peripheral.getType(side) == "modem" then rednet.open(side) end end end local blacklist = {"bbpack", "bbpack.lua", "startup", "startup.lua", ".settings", ".gif", ".zip", ".bbpack.cfg"} if not _G.rfs then local rfs, ramdisk = {}, {} for key, value in pairs(fs) do rfs[key] = value end local function clusterTalk(cluster, answer, ...) local target, uuid, result, sender, msg = clusterList[cluster], math.random(1, 0x7FFFFFFF), {} for i = 1, #target do rednet.send(target[i], {["cluster"] = cluster, ["uuid"] = uuid, unpack(arg)}, rednet.host and cluster) end if answer then for i = 1, #target do repeat sender, msg = rednet.receive(rednet.host and cluster) until type(msg) == "table" and msg.cluster == cluster and msg.uuid == uuid result[i] = msg[1] end return result end end _G.fs.list = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end path = fs.combine(path, "") local elements = dividePath(path) if not fs.isDir(path) then error("Not a directory", 2) end if fs.getDrive(path) == "hdd" then local results = rfs.list(path) for i = 1, #results do local thisResult = results[i] if thisResult:sub(-4) == ".bbp" then results[i] = thisResult:sub(1, -5) end end for mount in pairs(webMountList) do local mountElements = dividePath(mount) if #elements == #mountElements - 1 then local match = true for i = 1, #elements do if elements[i] ~= mountElements[i] then match = false break end end if match then results[#results + 1] = mountElements[#mountElements] end end end if path == "" then results[#results + 1] = "ram" for cluster in pairs(clusterList) do results[#results + 1] = cluster end for repo in pairs(repoList) do results[#results + 1] = repo end end table.sort(results) return results elseif clusterList[elements[1]] then local results = {} local lists = clusterTalk(elements[1], true, "list", path) for i = 1, #clusterList[elements[1]] do local subList = lists[i] for i = 1, #subList do local found, thisSub = false, subList[i] for j = 1, #results do if results[j] == thisSub then found = true break end end if not found then results[#results + 1] = thisSub end end end table.sort(results) return results elseif elements[1] == "ram" or repoList[elements[1]] then local cur, results = (elements[1] == "ram") and ramdisk or repoList[elements[1]], {} for i = 2, #elements do cur = cur[elements[i]] end for entry in pairs(cur) do results[#results + 1] = entry end table.sort(results) return results else return rfs.list(path) end end _G.fs.exists = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end path = fs.combine(path, "") local elements = dividePath(path) if webMountList[path] then return true elseif clusterList[elements[1]] then if #elements == 1 then return true end local list = clusterTalk(elements[1], true, "exists", path) for i = 1, #list do if list[i] then return true end end return false elseif elements[1] == "ram" or repoList[elements[1]] then local cur = (elements[1] == "ram") and ramdisk or repoList[elements[1]] for i = 2, #elements do cur = cur[elements[i]] if not cur then return false end end return true else return rfs.exists(path..".bbp") or rfs.exists(path) end end _G.fs.isDir = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end if not fs.exists(path) then return false end path = fs.combine(path, "") local elements = dividePath(path) if clusterList[elements[1]] then if #elements == 1 then return true end local list = clusterTalk(elements[1], true, "isDir", path) return list[1] elseif elements[1] == "ram" or repoList[elements[1]] then local cur = (elements[1] == "ram") and ramdisk or repoList[elements[1]] for i = 2, #elements do cur = cur[elements[i]] end return type(cur) == "table" else return rfs.isDir(path) end end _G.fs.isReadOnly = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end path = fs.combine(path, "") local elements = dividePath(path) if webMountList[path] or repoList[elements[1]] then return true elseif clusterList[elements[1]] or elements[1] == "ram" then return false else return rfs.isReadOnly(rfs.exists(path..".bbp") and (path..".bbp") or path) end end _G.fs.getDrive = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end path = fs.combine(path, "") local elements = dividePath(path) if clusterList[elements[1]] or elements[1] == "ram" or repoList[elements[1]] then return fs.exists(path) and elements[1] else return rfs.getDrive(rfs.exists(path..".bbp") and (path..".bbp") or path) end end _G.fs.getSize = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end path = fs.combine(path, "") local elements = dividePath(path) if webMountList[path] or repoList[elements[1]] then return 0 elseif clusterList[elements[1]] then if #elements == 1 then return 0 end if not fs.exists(path) then error("No such file", 2) end local size, list = 0, clusterTalk(elements[1], true, "getSize", path) for i = 1, #clusterList[elements[1]] do size = size + list[i] end return size elseif elements[1] == "ram" then local cur = ramdisk for i = 2, #elements do cur = cur[elements[i]] if not cur then error("No such file", 2) end end return type(cur) == "string" and #cur or 0 else return rfs.getSize(rfs.exists(path..".bbp") and (path..".bbp") or path) end end _G.fs.getFreeSpace = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end path = fs.combine(path, "") local elements = dividePath(path) if clusterList[elements[1]] then local size, list = 0, clusterTalk(elements[1], true, "getFreeSpace") for i = 1, #clusterList[elements[1]] do size = size + list[i][1] end return size elseif elements[1] == "ram" then return math.huge elseif repoList[elements[1]] then return 0 else return rfs.getFreeSpace(path) end end _G.fs.makeDir = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end path = fs.combine(path, "") local elements = dividePath(path) if fs.exists(path) then if fs.isDir(path) then return else error("File exists", 2) end end if clusterList[elements[1]] then clusterTalk(elements[1], false, "makeDir", path) elseif elements[1] == "ram" then local cur = ramdisk for i = 2, #elements do local next = cur[elements[i]] if next then cur = next else cur[elements[i]] = {} cur = cur[elements[i]] end end elseif repoList[elements[1]] then error("Access denied", 2) else return rfs.makeDir(path) end end _G.fs.move = function(path1, path2) if type(path1) ~= "string" then error("bad argument #1 (expected string, got " .. type(path1) .. ")", 2 ) end if type(path2) ~= "string" then error("bad argument #2 (expected string, got " .. type(path2) .. ")", 2 ) end path1, path2 = fs.combine(path1, ""), fs.combine(path2, "") if not fs.exists(path1) then error("No such file", 2) end if fs.exists(path2) then error("File exists", 2) end if fs.isReadOnly(path1) or fs.isReadOnly(path2) or (#dividePath(path1) == 1 and fs.getDrive(path1) ~= "hdd") then error("Access denied", 2) end if #dividePath(path1) < #dividePath(path2) and path2:sub(#path1) == path1 then error("Can't copy a directory inside itself", 2) end -- ... and if we run out of space we'll just let things fall over at the writing level. if fs.isDir(path1) then fs.makeDir(path2) local list = fs.list(path1) for i = 1, #list do fs.move(fs.combine(path1, list[i]), fs.combine(path2, list[i])) end else local input, output = fs.open(path1, "rb"), fs.open(path2, "wb") if input.readAll then output.write(input.readAll()) else for byte in input.read do output.write(byte) end end input.close() output.close() end fs.delete(path1) end _G.fs.copy = function(path1, path2) if type(path1) ~= "string" then error("bad argument #1 (expected string, got " .. type(path1) .. ")", 2 ) end if type(path2) ~= "string" then error("bad argument #2 (expected string, got " .. type(path2) .. ")", 2 ) end path1, path2 = fs.combine(path1, ""), fs.combine(path2, "") if not fs.exists(path1) then error("No such file", 2) end if fs.exists(path2) then error("File exists", 2) end if fs.isReadOnly(path2) then error("Access denied", 2) end if #dividePath(path1) < #dividePath(path2) and path2:sub(#path1) == path1 then error("Can't copy a directory inside itself", 2) end -- ... and if we run out of space we'll just let things fall over at the writing level. if fs.isDir(path1) then fs.makeDir(path2) local list = fs.list(path1) for i = 1, #list do fs.copy(fs.combine(path1, list[i]), fs.combine(path2, list[i])) end else local input, output = fs.open(path1, "rb"), fs.open(path2, "wb") if input.readAll then output.write(input.readAll()) else for byte in input.read do output.write(byte) end end input.close() output.close() end end _G.fs.delete = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end path = fs.combine(path, "") local elements = dividePath(path) if not fs.exists(path) then return end if webMountList[path] then webMountList[path] = nil local file = rfs.open(".bbpack.cfg", "w") file.write(textutils.serialize(configTable)) file.close() elseif clusterList[elements[1]] then if #elements == 1 then clusterList[elements[1]] = nil local file = rfs.open(".bbpack.cfg", "w") file.write(textutils.serialize(configTable)) file.close() else clusterTalk(elements[1], false, "delete", path) end elseif repoList[elements[1]] then if #elements == 1 then repoList[elements[1]], configTable.githubRepos[elements[1]] = nil, nil local file = rfs.open(".bbpack.cfg", "w") file.write(textutils.serialize(configTable)) file.close() else error("Access denied", 2) end elseif elements[1] == "ram" then if #elements == 1 then error("Access denied", 2) end local cur = ramdisk for i = 2, #elements - 1 do cur = cur[elements[i]] if not cur then return end end cur[elements[#elements]] = nil else if fs.isDir(path) then local list = fs.list(path) for i = 1, #list do local ok, err = pcall(fs.delete, fs.combine(path, list[i])) if not ok then error(err:gsub("pcall: ", ""), 2) end end return rfs.delete(path) else return rfs.delete(rfs.exists(path..".bbp") and (path..".bbp") or path) end end end _G.fs.open = function(path, mode) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end if type(mode) ~= "string" then error("bad argument #2 (expected string, got " .. type(mode) .. ")", 2 ) end path = fs.combine(path, "") local elements = dividePath(path) mode = mode:lower() local binary, append, read, write = mode:find("b") ~= nil, mode:find("a") ~= nil, mode:find("r") ~= nil, mode:find("w") ~= nil if webMountList[path] or clusterList[elements[1]] or elements[1] == "ram" or repoList[elements[1]] then if not (append or write or read) then error("Invalid file mode: " .. mode, 2) end if read then if not fs.exists(path) or fs.isDir(path) then return nil, "No such file" end local data local handle = {["close"] = function() data = nil end} if webMountList[path] then local webHandle = http.get(webMountList[path], nil, term.setPaletteColour and true) data = webHandle.readAll() webHandle.close() elseif clusterList[elements[1]] then data = {} local list = clusterTalk(elements[1], true, "get", path) for i = 1, #clusterList[elements[1]] do if list[i] then data[list[i][1]] = list[i][2] end end data = table.concat(data) data = decompressInternal({data:byte(1, #data)}, true) elseif repoList[elements[1]] then data = repoList[elements[1]] for i = 2, #elements do data = data[elements[i]] end local webHandle = http.get(data, nil, term.setPaletteColour and true) data = webHandle.readAll() webHandle.close() else data = ramdisk for i = 2, #elements do data = data[elements[i]] end end if #data == 0 then data = nil end if binary then handle.read = function(amount) if not data then return nil end local result if type(amount) ~= "number" then result = data:byte(1) data = data:sub(2) else result = data:sub(1, amount) data = data:sub(amount + 1) end if #data == 0 then data = nil end return result end handle.readAll = function() if not data then return nil end local result = data data = nil return result end else handle.readLine = function() if not data then return nil end if data:find("\n") then local result = data:sub(1, data:find("\n") - 1) data = data:sub(data:find("\n") + 1) if data:byte(1) == 13 then data = data:sub(2) end return result else local result = data data = nil return data end end handle.readAll = function() if not data then return nil end local result = data data = nil return result end end return handle elseif write or append then if webMountList[path] or repoList[elements[1]] then return nil, "Access denied" end if fs.isDir(path) then return nil, "Cannot write to directory" end fs.makeDir(fs.getDir(path)) local handle, output, counter = {}, {}, 1 if binary then handle.write = function(data) if type(data) ~= "string" and type(data) ~= "number" then error("bad argument #1 (string or number expected, got " .. type(data) .. ")", 2) end output[counter] = type(data) == "number" and string.char(data) or data counter = counter + 1 end else handle.write = function(data) output[counter] = tostring(data) counter = counter + 1 end handle.writeLine = function(data) output[counter] = tostring(data) output[counter + 1] = "\n" counter = counter + 2 end end local ramLink, ramIndex if clusterList[elements[1]] and append and fs.exists(path) then local data, list = {}, clusterTalk(elements[1], true, "get", path) for i = 1, #list do if list[i] then data[list[i][1]] = list[i][2] end end data = table.concat(data) output[1], counter = decompressInternal({data:byte(1, #data)}, true), 2 elseif elements[1] == "ram" then ramLink = ramdisk for i = 2, #elements - 1 do ramLink = ramLink[elements[i]] end ramIndex = elements[#elements] if (append and not ramLink[ramIndex]) or not append then ramLink[ramIndex] = "" end end handle.flush = function() if clusterList[elements[1]] then output, counter = {table.concat(output)}, 1 local data, segs, pos, totalSpace, thisCluster = string.char(unpack(compressInternal(output[1]))), 1, 1, fs.getFreeSpace(elements[1]), clusterList[elements[1]] if fs.exists(path) then totalSpace = totalSpace + fs.getSize(path) end if totalSpace < #data then error("Out of space", 2) end fs.delete(path) local spaceList = clusterTalk(elements[1], true, "getFreeSpace") for i = 1, #thisCluster do local thisSpace = spaceList[i][1] if thisSpace > 0 then local segString = tostring(segs) rednet.send(spaceList[i][2], {["cluster"] = elements[1], "put", path .. string.rep("0", 3 - #segString) .. segString, data:sub(pos, pos + thisSpace - 1)}, rednet.host and elements[1]) pos, segs = pos + thisSpace, segs + 1 end if pos > #data then break end end else output = table.concat(output) if append then output = ramLink[ramIndex] .. output end ramLink[ramIndex] = output output, counter, append = {}, 1, true end end handle.close = function() handle.flush() for key in pairs(handle) do handle[key] = function() end end end return handle end else if (write or append) and rfs.isReadOnly(path) then return nil, "Access denied" end for i = 1, #blacklist do local check = blacklist[i] if path:sub(-#check):lower() == check then return rfs.open(path, mode) end end if read then return rfs.exists(path .. ".bbp") and open(path .. ".bbp", mode) or rfs.open(path, mode) elseif configTable.compressedFS then if rfs.getDrive(elements[1]) and rfs.getDrive(elements[1]) ~= "hdd" then return rfs.open(path, mode) elseif append then if rfs.exists(path) then local file, content = rfs.open(path, binary and "rb" or "r") if file.readAll then content = file.readAll() else content = {} for byte in file.read do content[#content + 1] = byte end content = string.char(unpack(content)) end file.close() rfs.delete(path) file = open(path .. ".bbp", binary and "wb" or "w") file.write(content) return file else return open(path .. ".bbp", mode) end elseif write then if rfs.exists(path) then rfs.delete(path) end return open(path .. ".bbp", mode) end else if append then if rfs.exists(path .. ".bbp") then local file = open(path .. ".bbp", binary and "rb" or "r") local content = file.readAll() file.close() rfs.delete(path .. ".bbp") file = rfs.open(path, binary and "wb" or "w") if file.writeLine or term.setPaletteColour then file.write(content) else content = {string.byte(1, #content)} for i = 1, #content do file.write(content[i]) end end return file else return rfs.open(path, mode) end elseif write then if rfs.exists(path .. ".bbp") then rfs.delete(path .. ".bbp") end return rfs.open(path, mode) end end error("Unsupported mode", 2) end end _G.fs.find = function(path) if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end local pathParts, results, curfolder = {}, {}, "/" for part in path:gmatch("[^/]+") do pathParts[#pathParts + 1] = part:gsub("*", "[^/]*") end if #pathParts == 0 then return {} end local prospects = fs.list(curfolder) for i = 1, #prospects do prospects[i] = {["parent"] = curfolder, ["depth"] = 1, ["name"] = prospects[i]} end while #prospects > 0 do local thisProspect = table.remove(prospects, 1) local fullPath = fs.combine(thisProspect.parent, thisProspect.name) if thisProspect.name == thisProspect.name:match(pathParts[thisProspect.depth]) then if thisProspect.depth == #pathParts then results[#results + 1] = fullPath elseif fs.isDir(fullPath) and thisProspect.depth < #pathParts then local newList = fs.list(fullPath) for i = 1, #newList do prospects[#prospects + 1] = {["parent"] = fullPath, ["depth"] = thisProspect.depth + 1, ["name"] = newList[i]} end end end end return results end _G.rfs = rfs end update = bbpack and bbpack.update or function() for cluster, ids in pairs(clusterList) do for i = 1, #ids do rednet.send(ids[i], {["cluster"] = cluster, "update"}, rednet.host and cluster) end end local file = rfs.open("bbpack", "w") file.write(downloadPasteInternal("cUYTGbpb")) file.close() os.reboot() end fileSys = bbpack and bbpack.fileSys or function(par1, par2) if type(par1) == "boolean" or (type(par1) == "string" and type(par2) == "boolean") then -- Compress / decompress hdd contents. local list if type(par1) == "boolean" then list = rfs.list("") configTable.compressedFS = par1 else list = {par1} par1 = par2 end while #list > 0 do local entry = list[#list] list[#list] = nil if rfs.getDrive(entry) == "hdd" and not webMountList[entry] then if rfs.isDir(entry) then local newList, curLen = rfs.list(entry), #list for i = 1, #newList do list[curLen + i] = fs.combine(entry, newList[i]) end else local blacklisted = false for i = 1, #blacklist do local check = blacklist[i] if entry:sub(-#check):lower() == check then blacklisted = true break end end if not blacklisted then if par1 and entry:sub(-4) ~= ".bbp" then -- Compress this file. local file, content = rfs.open(entry, "rb") if file.readAll then content = file.readAll() else content = {} local counter = 1 for byte in file.read do content[counter] = byte counter = counter + 1 end content = string.char(unpack(content)) end file.close() content = compressInternal(content) if rfs.getFreeSpace(entry) + rfs.getSize(entry) < #content then return false end rfs.delete(entry) snooze() file = rfs.open(entry .. ".bbp", "wb") if term.setPaletteColor then file.write(string.char(unpack(content))) else for i = 1, #content do file.write(content[i]) end end file.close() snooze() elseif not par1 and entry:sub(-4) == ".bbp" then -- Decompress this file. local file = open(entry, "rb") local content = file.readAll() file.close() if rfs.getFreeSpace(entry) + rfs.getSize(entry) < #content then return false end rfs.delete(entry) snooze() file = rfs.open(entry:sub(1, -5), "wb") if term.setPaletteColor then file.write(content) else content = {content:byte(1, #content)} for i = 1, #content do file.write(content[i]) end end file.close() snooze() end end end end end elseif type(par1) == "string" and type(par2) == "string" then -- New web mount. local url, path = par1, fs.combine(par2, "") local elements = dividePath(path) local repo = getGithubRepo(url) if repo then if #elements > 1 then error("bbpack.mount: Github repos must be mounted at the root of your file system", 2) end repoList[path] = repo configTable.githubRepos[path] = url else if fs.getDrive(elements[1]) and fs.getDrive(elements[1]) ~= "hdd" then error("bbpack.mount: web mounts must be located on the main hdd", 2) end local get = http.get(url) if not get then error("bbpack.mount: Can't connect to URL: "..url, 2) end get.close() webMountList[path] = url end elseif type(par1) == "string" then -- New cluster mount. local cluster, uuid = par1, math.random(1, 0x7FFFFFFF) for _, side in pairs(rs.getSides()) do if peripheral.getType(side) == "modem" then rednet.open(side) end end rednet.broadcast({["cluster"] = cluster, ["uuid"] = uuid, "rollcall"}, rednet.host and cluster) clusterList[cluster] = nil local myTimer, map = os.startTimer(5), {} while true do local event, par1, par2 = os.pullEvent() if event == "timer" and par1 == myTimer then break elseif event == "rednet_message" and type(par2) == "table" and par2.cluster == cluster and par2.uuid == uuid and par2[1] == "rollcallResponse" then map[#map + 1] = par1 end end if #map == 0 then error("bbpack.mount: Can't connect to cluster: " .. cluster, 2) end clusterList[cluster] = map end local file = rfs.open(".bbpack.cfg", "w") file.write(textutils.serialize(configTable)) file.close() return true end end