· 4 years ago · Apr 05, 2021, 02:56 AM
1-- +---------------------+------------+---------------------+
2-- |                     |            |                     |
3-- |                     |   BBPack   |                     |
4-- |                     |            |                     |
5-- +---------------------+------------+---------------------+
6
7local version = "Version 1.6.1"
8
9-- Pastebin uploader/downloader for ComputerCraft, by Jeffrey Alexander (aka Bomb Bloke).
10-- Handles multiple files in a single paste, as well as non-ASCII symbols within files.
11-- Used to be called "package".
12-- http://www.computercraft.info/forums2/index.php?/topic/21801-
13-- pastebin get cUYTGbpb bbpack
14
15---------------------------------------------
16------------Variable Declarations------------
17---------------------------------------------
18
19local band, brshift, blshift = bit.band, bit.brshift, bit.blshift
20
21local b64 = {}
22for i = 1, 64 do
23	b64[i - 1] = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"):byte(i)
24	b64[("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"):sub(i, i)] = i - 1
25end
26
27---------------------------------------------
28------------Function Declarations------------
29---------------------------------------------
30
31local unpack = unpack or table.unpack
32
33local function snooze()
34	local myEvent = tostring({})
35	os.queueEvent(myEvent)
36	os.pullEvent(myEvent)
37end
38
39local function toBase64Internal(inputlist)
40	if type(inputlist) ~= "table" then error("bbpack.toBase64: Expected: table or file handle", 2) end
41
42	if inputlist.read then
43		local templist, len = {}, 1
44
45		for byte in inputlist.read do
46			templist[len] = byte
47			len = len + 1
48		end
49
50		inputlist.close()
51		inputlist = templist
52	elseif inputlist.readLine then
53		inputlist.close()
54		error("bbpack.toBase64: Use a binary-mode file handle", 2)
55	end
56
57	if #inputlist == 0 then return "" end
58
59	local curbit, curbyte, outputlist, len = 32, 0, {}, 1
60
61	for i = 1, #inputlist do
62		local inByte, mask = inputlist[i], 128
63
64		for j = 1, 8 do
65			if band(inByte, mask) == mask then curbyte = curbyte + curbit end
66			curbit, mask = curbit / 2, mask / 2
67
68			if curbit < 1 then
69				outputlist[len] = b64[curbyte]
70				curbit, curbyte, len = 32, 0, len + 1
71			end
72		end
73	end
74
75	if curbit > 1 then outputlist[len] = b64[curbyte] end
76
77	return string.char(unpack(outputlist))
78end
79
80local function fromBase64Internal(inData)
81	if type(inData) ~= "string" and type(inData) ~= "table" then error("bbpack.fromBase64: Expected: string or file handle", 2) end
82
83	if type(inData) == "table" then
84		if inData.readLine then
85			local temp = inData.readAll()
86			inData.close()
87			inData = temp
88		else
89			if inData.close then inData.close() end
90			error("bbpack.fromBase64: Use text-mode file handles", 2)
91		end
92	end
93
94	if #inData == 0 then return {} end
95
96	local curbyte, curbit, outputlist, len = 0, 128, {}, 1
97
98	for i = 1, #inData do
99		local mask, curchar = 32, b64[inData:sub(i, i)]
100
101		for j = 1, 6 do
102			if band(curchar, mask) == mask then curbyte = curbyte + curbit end
103			curbit, mask = curbit / 2, mask / 2
104
105			if curbit < 1 then
106				outputlist[len] = curbyte
107				curbit, curbyte, len = 128, 0, len + 1
108			end
109		end
110	end
111
112	if curbit > 1 and curbyte > 0 then outputlist[len] = curbyte end
113
114	return outputlist
115end
116
117local function compressIterator(ClearCode)
118	local startCodeSize = 1
119	while math.pow(2, startCodeSize) < ClearCode do startCodeSize = startCodeSize + 1 end
120
121	local EOI, ClearCode = math.pow(2, startCodeSize) + 1, math.pow(2, startCodeSize)
122	startCodeSize = startCodeSize + 1
123
124	local curstring, len, curbit, curbyte, outputlist, codes, CodeSize, MaxCode, nextcode, curcode = "", 2, 1, 0, {0}, {}, startCodeSize, math.pow(2, startCodeSize) - 1, EOI + 1
125
126	local function packByte(num)
127		local mask = 1
128
129		for i = 1, CodeSize do
130			if band(num, mask) == mask then curbyte = curbyte + curbit end
131			curbit, mask = curbit * 2, mask * 2
132
133			if curbit > 128 or (i == CodeSize and num == EOI) then
134				local counter = blshift(brshift(#outputlist - 1, 8), 8) + 1
135				outputlist[counter] = outputlist[counter] + 1
136
137				if outputlist[counter] > 255 then
138					outputlist[counter], outputlist[counter + 256], len = 255, 1, len + 1
139					snooze()
140				end
141
142				outputlist[len] = curbyte
143				curbit, curbyte, len = 1, 0, len + 1
144			end
145		end
146	end
147
148	packByte(ClearCode)
149
150	return function(incode)
151		if not incode then
152			if curcode then packByte(curcode) end
153			packByte(EOI)
154			outputlist[#outputlist + 1] = 0
155			return outputlist
156		end
157
158		if not curcode then
159			curcode = incode
160			return
161		end
162
163		curstring = curstring .. string.char(incode)
164		local thisCode = codes[curstring]
165
166		if thisCode then
167			curcode = thisCode
168		else
169			codes[curstring] = nextcode
170			nextcode = nextcode + 1
171
172			packByte(curcode)
173
174			if nextcode == MaxCode + 2 then
175				CodeSize = CodeSize + 1
176				MaxCode = math.pow(2, CodeSize) - 1
177			end
178
179			if nextcode == 4095 then
180				packByte(ClearCode)
181				CodeSize, MaxCode, nextcode, codes = startCodeSize, math.pow(2, startCodeSize) - 1, EOI + 1, {}
182			end
183
184			curcode, curstring = incode, string.char(incode)
185		end
186	end
187end
188
189local function compressInternal(inputlist, valRange)
190	if type(inputlist) ~= "table" and type(inputlist) ~= "string" then error("bbpack.compress: Expected: table, string or file handle", 2) end
191
192	if not valRange then valRange = 256 end
193	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
194
195	if type(inputlist) == "table" and inputlist.close then
196		local templist
197		if inputlist.readAll then
198			templist = inputlist.readAll()
199		else
200			local len = 1
201			templist = {}
202			for thisByte in inputlist.read do
203				templist[len] = thisByte
204				len = len + 1
205			end
206		end
207		inputlist.close()
208		inputlist = templist
209	end
210
211	if type(inputlist) == "string" then inputlist = {inputlist:byte(1, #inputlist)} end
212
213	if #inputlist == 0 then return {} end
214
215	local compressIt = compressIterator(valRange)
216
217	local sleepCounter = 0
218	for i = 1, #inputlist do
219		compressIt(inputlist[i])
220
221		sleepCounter = sleepCounter + 1
222		if sleepCounter > 1023 then
223			sleepCounter = 0
224			snooze()
225		end
226	end
227
228	return compressIt(false)
229end
230
231local function decompressIterator(ClearCode, codelist)
232	local startCodeSize = 1
233	while math.pow(2, startCodeSize) < ClearCode do startCodeSize = startCodeSize + 1 end
234
235	local EOI, ClearCode = math.pow(2, startCodeSize) + 1, math.pow(2, startCodeSize)
236	startCodeSize = startCodeSize + 1
237
238	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
239	for i = 0, ClearCode - 1 do codes[i] = string.char(i) end
240
241	return function()
242		while true do
243			local curcode, curbit = 0, 1
244
245			for i = 1, CodeSize do
246				if band(curbyte, maskbit) == maskbit then curcode = curcode + curbit end
247				curbit, maskbit = curbit * 2, maskbit * 2
248
249				if maskbit > 128 and not (i == CodeSize and curcode == EOI) then
250					maskbit, curbyte, gotbytes = 1, codelist[spot], gotbytes + 1
251					spot = spot + 1
252
253					if gotbytes > lastcounter then
254						if curbyte == 0 then break end
255						lastcounter, gotbytes = curbyte, 1
256						curbyte = codelist[spot]
257						spot = spot + 1
258						snooze()
259					end
260				end
261			end
262
263			if curcode == ClearCode then
264				CodeSize, MaxCode, nextcode, codes = startCodeSize, math.pow(2, startCodeSize) - 1, EOI + 1, {}
265				for i = 0, ClearCode - 1 do codes[i] = string.char(i) end
266			elseif curcode ~= EOI then
267				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
268
269				if nextcode < 4096 then
270					codes[nextcode] = codes[curcode]
271					nextcode = nextcode + 1
272				end
273
274				if nextcode - 2 == MaxCode then
275					CodeSize = CodeSize + 1
276					MaxCode = math.pow(2, CodeSize) - 1
277				end
278
279				return codes[curcode]
280			else return end
281		end
282	end
283end
284
285local function decompressInternal(codelist, outputText, valRange)
286	if type(codelist) ~= "table" then error("bbpack.decompress: Expected: table or file handle", 2) end
287
288	if not valRange then valRange = 256 end
289	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
290
291	if codelist.readLine then
292		codelist.close()
293		error("bbpack.decompress: Use binary-mode file handles", 2)
294	elseif codelist.readAll then
295		codelist = codelist.readAll()
296		codelist = {codelist:byte(1, #codelist)}
297	elseif codelist.read then
298		local data, len = {}, 1
299		while true do
300			local amount = codelist.read()
301			data[len] = amount
302			len = len + 1
303
304			if amount == 0 then break end
305
306			for i = 1, amount do
307				data[len] = codelist.read()
308				len = len + 1
309			end
310
311			snooze()
312		end
313		codelist = data
314	elseif #codelist == 0 then return outputText and "" or {} end
315
316	local outputlist, decompressIt, len = {}, decompressIterator(valRange, codelist), 1
317
318	local sleepCounter = 0
319	while true do
320		local output = decompressIt()
321
322		if output then
323			outputlist[len] = output
324			len = len + 1
325		else break end
326	end
327
328	outputlist = table.concat(outputlist)
329
330	return outputText and outputlist or {outputlist:byte(1, #outputlist)}
331end
332
333local function uploadPasteInternal(name, content)
334	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
335
336	if type(content) == "table" then
337		if content.readLine then
338			local temp = content.readAll()
339			content.close()
340			content = temp
341		else
342			if content.close then content.close() end
343			error("bbpack.uploadPaste: Use text-mode file handles", 2)
344		end
345	end
346
347	local webHandle = http.post(
348		"https://pastebin.com/api/api_post.php", 
349		"api_option=paste&" ..
350		"api_dev_key=147764e5c6ac900a3015d77811334df1&" ..
351		"api_paste_format=lua&" ..
352		"api_paste_name=" .. textutils.urlEncode(name) .. "&" ..
353		"api_paste_code=" .. textutils.urlEncode(content))
354
355	if webHandle then
356		local response = webHandle.readAll()
357		webHandle.close()
358		return string.match(response, "[^/]+$")
359	else error("Connection to pastebin failed. http API config in ComputerCraft.cfg is enabled, but may be set to block pastebin - or pastebin servers may be busy.") end
360end
361
362local function downloadPasteInternal(pasteID)
363	if type(pasteID) ~= "string" then error("bbpack.downloadPaste: Expected: (string) paste ID", 2) end
364
365	local webHandle = http.get("https://pastebin.com/raw/" .. textutils.urlEncode(pasteID))
366
367	if webHandle then
368		local incoming = webHandle.readAll()
369		webHandle.close()
370		return incoming
371	else error("Connection to pastebin failed. http API config in ComputerCraft.cfg is enabled, but may be set to block pastebin - or pastebin servers may be busy.") end
372end
373
374if shell then
375	---------------------------------------------
376	------------     Main Program    ------------
377	---------------------------------------------
378
379	if not bbpack then os.loadAPI("bbpack") end
380
381	local args = {...}
382	if #args > 0 then args[1] = args[1]:lower() end
383
384	if #args < 1 or not (args[1] == "put" or args[1] == "get" or args[1] == "fileput" or args[1] == "fileget" or args[1] == "mount" or args[1] == "compress" or args[1] == "decompress" or args[1] == "cluster" or args[1] == "update") then
385		textutils.pagedPrint("Usage:\n")
386
387		textutils.pagedPrint("Uploads specified file or directory:")
388		textutils.pagedPrint("bbpack put [file/directory name]\n")
389
390		textutils.pagedPrint("Dumps paste into specified file or directory:")
391		textutils.pagedPrint("bbpack get <pasteID> [file/directory name]\n")
392
393		textutils.pagedPrint("Writes specified file or directory to archive file:")
394		textutils.pagedPrint("bbpack fileput [file/directory name] <target file>\n")
395
396		textutils.pagedPrint("Unpacks archive file to specified file or directory:")
397		textutils.pagedPrint("bbpack fileget <source file> [file/directory name]\n")
398
399		print("For the above options, if [file/directory name] is omitted the root of the drive will be used instead.\n")
400
401		textutils.pagedPrint("Enables automatic compression on hdd and compresses all existing files:")
402		textutils.pagedPrint("bbpack compress\n")
403
404		textutils.pagedPrint("Disables automatic compression on hdd and decompresses all existing files:")
405		textutils.pagedPrint("bbpack decompress\n")
406
407		textutils.pagedPrint("Mounts a given URL as the specified local file:")
408		textutils.pagedPrint("bbpack mount <URL> <fileName>\n")
409
410		textutils.pagedPrint("Turns the system into a server for the specified cluster:")
411		textutils.pagedPrint("bbpack cluster <clusterName>\n")
412
413		textutils.pagedPrint("Mounts the specified cluster as a drive:")
414		textutils.pagedPrint("bbpack mount <clusterName>\n")
415
416		textutils.pagedPrint("Updates package, reboots, and instructs all mounted cluster servers to do the same:")
417		textutils.pagedPrint("bbpack update\n")
418
419		return
420	end
421
422	if (args[1] == "put" or args[1] == "get") and not http then
423		print("BBPack's pastebin functionality requires that the http API be enabled.")
424		print("Shut down MineCraft game/server, set http_enable to true in ComputerCraft.cfg, then restart.")
425		error()
426	end
427
428	---------------------------------------------
429	------------      Uploading      ------------
430	---------------------------------------------
431
432	if args[1] == "put" or args[1] == "fileput" then
433		local toFile
434		if args[1] == "fileput" then toFile = table.remove(args, #args) end
435
436		local uploadName, parent
437
438		if not args[2] then
439			print("Full system upload - are you sure? (y/n)")
440			if read():sub(1, 1):lower() ~= "y" then
441				print("Aborted.")
442				error()
443			end
444
445			uploadName = os.getComputerLabel()
446			if not uploadName then
447				print("Enter paste title:")
448				uploadName = read()
449			end
450
451			args[2] = ""
452		end
453
454		local target, output = shell.resolve(args[2]), {}
455		uploadName = uploadName or target
456
457		if not fs.exists(target) then
458			print("Invalid target.")
459			error()
460		end
461
462		if fs.isDir(target) then
463			local fileList = fs.list(target)
464			parent = target
465			while #fileList > 0 do
466				if fs.isDir(shell.resolve(fs.combine(parent, fileList[#fileList]))) then
467					local thisDir = table.remove(fileList, #fileList)
468					local newList = fs.list(shell.resolve(fs.combine(parent, thisDir)))
469					for i = 1, #newList do fileList[#fileList + 1] = fs.combine(thisDir, newList[i]) end
470					if #newList == 0 then output[#output + 1] = thisDir end
471				else output[#output + 1] = table.remove(fileList, #fileList) end
472			end
473			target = output
474			output = {}
475		else parent, target = "", {target} end
476
477		snooze()
478
479		for i = #target, 1, -1 do
480			if fs.combine(parent, target[i]) ~= shell.getRunningProgram() and not (parent == "" and fs.getDrive(target[i]) ~= "hdd") then
481				if fs.isDir(fs.combine(parent, target[i])) then
482					print(target[i])
483					output[#output + 1] = {target[i], true}
484				else
485					print(target[i])
486					output[#output + 1] = {target[i], toBase64Internal(compressInternal(fs.open(fs.combine(parent, target[i]), "rb")))}
487					snooze()
488				end
489			end
490		end
491
492		if toFile then
493			output = textutils.serialize(output)
494			if fs.getFreeSpace(shell.dir()) < #output then error("Output "..#output.." bytes, disk space available "..fs.getFreeSpace(shell.dir()).." bytes: file not written.") end
495
496			write("Writing to file \"" .. toFile .. "\"... ")
497
498			toFile = fs.open(shell.resolve(toFile), "w")
499			toFile.write(output)
500			toFile.close()
501			print("Success.")
502		else
503			write("Connecting to pastebin.com... ")
504			local response = uploadPasteInternal(uploadName, textutils.serialize(output))
505			print("Success.")
506
507			print("Uploaded to paste ID: " .. response)
508			print("Run \"bbpack get " .. response .. (parent == "" and "" or " " .. parent) .. "\" to download.")
509		end
510
511	---------------------------------------------
512	------------     Downloading     ------------
513	---------------------------------------------
514
515	elseif args[1] == "get" or args[1] == "fileget" then
516		local incoming
517		if args[1] == "fileget" then
518			write("Attempting to read from archive... ")
519			if not fs.exists(shell.resolve(args[2])) then error("Can't find \"" .. shell.resolve(args[2]) .. "\".") end
520			local inFile = fs.open(shell.resolve(args[2]), "r")
521			incoming = textutils.unserialize(inFile.readAll())
522			inFile.close()
523			print("Success.")
524		else
525			write("Connecting to pastebin.com... ")
526			incoming = textutils.unserialize(downloadPasteInternal(args[2]))
527			print("Downloaded.")
528		end
529
530		local function getParent(path)
531			local pos = #path
532			if path:sub(pos,pos) == "/" then pos = pos - 1 end
533			while pos > 0 and path:sub(pos,pos) ~= "/" do pos = pos - 1 end
534			return pos > 0 and path:sub(1, pos - 1) or ""
535		end
536
537		local function writeFile(filePath, fileContent)
538			local path = fs.combine(shell.resolve("."), filePath)
539			if not fs.exists(getParent(path)) then fs.makeDir(getParent(path)) end
540			if fs.getFreeSpace(shell.dir()) < #fileContent then error(path.." "..#fileContent.." bytes, disk space available "..fs.getFreeSpace(shell.dir()).." bytes: file not written.") end
541
542			snooze()
543
544			local myFile = fs.open(path, "wb")
545			for i = 1, #fileContent do myFile.write(fileContent[i]) end
546			myFile.close()
547
548			snooze()
549		end
550
551		args[3] = args[3] or ""
552		if args[3] ~= "" and #incoming == 1 then
553			print(incoming[1][1] .. " => "..args[3])
554			writeFile(args[3], decompressInternal(fromBase64Internal(incoming[1][2])))
555		else
556			for i = 1, #incoming do
557				print(incoming[i][1])
558				if type(incoming[i][2]) == "string" then
559					writeFile(fs.combine(args[3], incoming[i][1]), decompressInternal(fromBase64Internal(incoming[i][2])))
560				else fs.makeDir(fs.combine(shell.resolve("."), fs.combine(args[3], incoming[i][1]))) end
561				incoming[i] = nil
562			end	
563		end
564
565	---------------------------------------------
566	------------     Compress FS     ------------
567	---------------------------------------------
568
569	elseif args[1] == "compress" then
570		bbpack.fileSys(true)
571		print("Filesystem compression enabled.")
572
573	---------------------------------------------
574	------------    Decompress FS    ------------
575	---------------------------------------------
576
577	elseif args[1] == "decompress" then
578		print(bbpack.fileSys(false) and "Filesystem compression disabled." or "Filesystem compression disabled, but space is insufficient to decompress all files.")
579
580	---------------------------------------------
581	------------        Mount        ------------
582	---------------------------------------------
583
584	elseif args[1] == "mount" then
585		bbpack.fileSys(args[2], args[3] and shell.resolve(args[3]))
586		print("Successfully mounted.")
587
588	---------------------------------------------
589	------------       Cluster       ------------
590	---------------------------------------------
591
592	elseif args[1] == "cluster" then
593		local cluster, protocol = args[2], rednet.host and args[2]
594
595		rfs.makeDir(cluster)
596
597		for _, side in pairs(rs.getSides()) do if peripheral.getType(side) == "modem" then rednet.open(side) end end
598
599		print("Running as part of cluster \"" .. cluster .. "\"...")
600
601		local function locFile(path)
602			local matches = rfs.find(path .. "*")
603
604			for i = 1, #matches do
605				local thisMatch = matches[i]
606				if #thisMatch == #path + 3 and thisMatch:sub(1, #path) == path then return thisMatch end
607			end
608
609			return nil
610		end
611		
612		return (function() while true do
613			local sender, msg = rednet.receive(protocol)
614
615			if type(msg) == "table" and msg.cluster == cluster then
616				local command, par1, par2 = unpack(msg)
617
618				if command == "rollcall" then
619					rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, "rollcallResponse"}, protocol)
620				elseif command == "isDir" then
621					rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, rfs.isDir(par1)}, protocol)
622				elseif command == "makeDir" then
623					rfs.makeDir(par1)
624				elseif command == "exists" then
625					rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, type(locFile(par1)) == "string"}, protocol)
626				elseif command == "getFreeSpace" then
627					rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, {rfs.getFreeSpace("") - 10000, os.getComputerID()}}, protocol)
628				elseif command == "getSize" then
629					local path = locFile(par1)
630					rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, path and rfs.getSize(path) or 0}, protocol)
631				elseif command == "delete" then
632					local path = locFile(par1)
633					if path then rfs.delete(path) end
634				elseif command == "list" then
635					local list = rfs.list(par1)
636
637					for i = 1, #list do
638						local entry = list[i]
639						if not fs.isDir(fs.combine(par1, entry)) then list[i] = entry:sub(1, -4) end
640					end
641
642					rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, list}, protocol)
643				elseif command == "get" then
644					local path = locFile(par1)
645
646					if path then
647						local file, content = rfs.open(path, "rb")
648
649						if file.readAll then
650							content = file.readAll()
651						else
652							content = {}
653							local counter = 1
654							for byte in file.read do
655								content[counter] = byte
656								counter = counter + 1
657							end
658							content = string.char(unpack(content))
659						end
660
661						file.close()
662
663						rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, {tonumber(path:sub(-3)), content}}, protocol)
664					end
665					
666					rednet.send(sender, {["cluster"] = cluster, ["uuid"] = msg.uuid, false}, protocol)
667				elseif command == "put" then
668					local file = rfs.open(par1, "wb")
669
670					if term.setPaletteColour then
671						file.write(par2)
672					else
673						par2 = {par2:byte(1, #par2)}
674						for i = 1, #par2 do file.write(par2[i]) end
675					end
676
677					file.close()
678				elseif command == "update" then
679					local file = rfs.open("bbpack", "w")
680					file.write(downloadPasteInternal("cUYTGbpb"))
681					file.close()
682
683					os.reboot()
684				end
685			end
686		end end)()
687
688	---------------------------------------------
689	------------        Update       ------------
690	---------------------------------------------
691
692	elseif args[1] == "update" then
693		bbpack.update()
694	end
695else
696	---------------------------------------------
697	------------     Load As API     ------------
698	---------------------------------------------
699
700	compress =      compressInternal
701	decompress =    decompressInternal
702
703	toBase64 =      toBase64Internal
704	fromBase64 =    fromBase64Internal
705
706	uploadPaste =   uploadPasteInternal
707	downloadPaste = downloadPasteInternal
708
709	function open(file, mode, valRange)
710		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
711
712		mode = mode:lower()
713		local binary, append, read, write, newhandle = mode:find("b") ~= nil, mode:find("a") ~= nil, mode:find("r") ~= nil, mode:find("w") ~= nil, {}
714
715		if not valRange then valRange = 256 end
716		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
717
718		if not (append or write or read) then error("bbpack.open: Invalid file mode: " .. mode, 2) end
719
720		if type(file) == "string" then
721			if append and rfs.exists(file) then
722				local oldfile = open(file, binary and "rb" or "r", valRange)
723				if not oldfile then return nil end
724				local olddata = oldfile.readAll()
725				oldfile.close()
726
727				newhandle = open(file, binary and "wb" or "w", valRange)
728				newhandle.write(olddata)
729				return newhandle
730			end
731
732			file = rfs.open(file, (read and "r" or "w") .. "b")
733			if not file then return nil end
734		else
735			if (write and (file.writeLine or not file.write)) or (read and not file.read) then error("bbpack.open: Handle / mode mismatch.", 2) end
736
737			local tempfile, keys = {}, {}
738
739			for key, _ in pairs(file) do keys[#keys + 1] = key end
740			for i = 1, #keys do
741				tempfile[keys[i]] = file[keys[i]]
742				file[keys[i]] = nil
743			end
744
745			file = tempfile
746		end
747
748		if read then
749			local data = {}
750			if file.readAll then
751				local len = 1
752
753				while true do
754					local amount = file.read()
755					data[len] = string.char(amount)
756					len = len + 1
757
758					if amount == 0 then break end
759
760					data[len] = file.read(amount)
761					len = len + 1
762				end
763
764				data = table.concat(data)
765				data = {data:byte(1, #data)}
766			else
767				local len = 1
768
769				while true do
770					local amount = file.read()
771					data[len] = amount
772					len = len + 1
773
774					if amount == 0 then break end
775
776					for i = 1, amount do
777						data[len] = file.read()
778						len = len + 1
779					end
780
781					snooze()
782				end
783			end
784
785			local decompressIt, outputlist = decompressIterator(valRange, data), ""
786
787			if binary then
788				function newhandle.read(amount)
789					if not outputlist then return nil end
790
791					if type(amount) ~= "number" then
792						if #outputlist == 0 then
793							outputlist = decompressIt()
794							if not outputlist then return nil end
795						end
796
797						local result = outputlist:byte(1)
798						outputlist = outputlist:sub(2)
799						return result
800					else
801						while #outputlist < amount do
802							local new = decompressIt()
803
804							if not new then
805								new = outputlist
806								outputlist = nil
807								if #new > 0 then return new else return end
808							end
809
810							outputlist = outputlist .. new
811						end
812
813						local result = outputlist:sub(1, amount)
814						outputlist = outputlist:sub(amount + 1)
815						return result
816					end
817				end
818
819				function newhandle.readAll()
820					if not outputlist then return nil end
821
822					local result, len = {outputlist}, 2
823					for data in decompressIt do
824						result[len] = data
825						len = len + 1
826					end
827
828					outputlist = nil
829
830					return table.concat(result)
831				end
832			else
833				function newhandle.readLine()
834					if not outputlist then return nil end
835
836					while not outputlist:find("\n") do
837						local new = decompressIt()
838
839						if not new then
840							new = outputlist
841							outputlist = nil
842							if #new > 0 then return new else return end
843						end
844
845						outputlist = outputlist .. new
846					end
847
848					local result = outputlist:sub(1, outputlist:find("\n") - 1)
849					outputlist = outputlist:sub(outputlist:find("\n") + 1)
850
851					if outputlist:byte(1) == 13 then outputlist = outputlist:sub(2) end
852
853					return result
854				end
855
856				function newhandle.readAll()
857					if not outputlist then return nil end
858
859					local result, len = {outputlist}, 2
860					for data in decompressIt do
861						result[len] = data
862						len = len + 1
863					end
864
865					outputlist = nil
866
867					return table.concat(result)
868				end
869			end
870
871			function newhandle.extractHandle()
872				local keys = {}
873				for key, _ in pairs(newhandle) do keys[#keys + 1] = key end
874				for i = 1, #keys do newhandle[keys[i]] = nil end
875				return file
876			end
877		else
878			local compressIt = compressIterator(valRange)
879
880			if binary then
881				function newhandle.write(data)
882					if type(data) == "number" then
883						compressIt(data)
884					elseif type(data) == "string" then
885						data = {data:byte(1, #data)}
886						for i = 1, #data do compressIt(data[i]) end
887					else error("bbpackHandle.write: bad argument #1 (string or number expected, got " .. type(data) .. ")", 2) end
888				end
889			else
890				function newhandle.write(text)
891					text = tostring(text)
892					text = {text:byte(1, #text)}
893					for i = 1, #text do compressIt(text[i]) end
894				end
895
896				function newhandle.writeLine(text)
897					text = tostring(text)
898					text = {text:byte(1, #text)}
899					for i = 1, #text do compressIt(text[i]) end
900					compressIt(10)
901				end
902			end
903
904			newhandle.flush = file.flush
905
906			function newhandle.extractHandle()
907				local output, fWrite = compressIt(false), file.write
908				for j = 1, #output do fWrite(output[j]) end
909				local keys = {}
910				for key, _ in pairs(newhandle) do keys[#keys + 1] = key end
911				for i = 1, #keys do newhandle[keys[i]] = nil end
912				return file
913			end
914		end
915
916		function newhandle.close()
917			newhandle.extractHandle().close()
918		end
919
920		return newhandle
921	end
922
923	function lines(file)
924		if type(file) == "string" then
925			file = open(file, "r")
926		elseif type(file) ~= "table" or not file.readLine then
927			error("bbpack.lines: Expected: file (string or \"r\"-mode handle).", 2)
928		end
929
930		return function()
931			if not file.readLine then return nil end
932
933			local line = file.readLine()
934			if line then
935				return line
936			else
937				file.close()
938				return nil
939			end
940		end
941	end
942	
943	local function dividePath(path)
944		local result = {}
945		for element in path:gmatch("[^/]+") do result[#result + 1] = element end
946		return result
947	end
948	
949	local function getGithubRepo(repo)
950		local elements = dividePath(repo)
951		for i = 1, #elements do if table.remove(elements, 1) == "github.com" then break end end
952		if #elements < 2 or elements[3] == "raw" then return end
953		repo = elements[1] .. "/" .. elements[2]
954		local branch = (elements[3] == "tree") and elements[4] or "master"
955		
956		local webHandle = http.get("https://api.github.com/repos/" .. repo .. "/git/trees/" .. branch .. "?recursive=1")
957		if not webHandle then return end
958		local json = textutils.unserialize(webHandle.readAll():gsub("\10", ""):gsub(" ", ""):gsub("%[", "{"):gsub("]", "}"):gsub("{\"", "{[\""):gsub(",\"", ",[\""):gsub("\":", "\"]="))
959		webHandle.close()
960		if json.message == "Not Found" then return end
961		
962		local tree, results = json.tree, {}
963		
964		for i = 1, #tree do if tree[i].type == "blob" then
965			local path, cur = tree[i].path, results
966			local elements = dividePath(path)
967			
968			for i = 1, #elements - 1 do
969				local element = elements[i]
970				if not cur[element] then cur[element] = {} end
971				cur = cur[element]
972			end
973			
974			cur[elements[#elements]] = "https://raw.githubusercontent.com/" .. repo .. "/" .. branch .. "/" .. path
975		end end
976		
977		if #elements > 4 then for i = 5, #elements do results = results[elements[i]] end end
978		
979		return (type(results) == "table") and results
980	end
981	
982	local configTable = {["webMounts"] = {}, ["githubRepos"] = {}, ["clusters"] = {}, ["compressedFS"] = false}
983
984	if fs.exists(".bbpack.cfg") then
985		local file = rfs and rfs.open(".bbpack.cfg", "r") or fs.open(".bbpack.cfg", "r")
986		local input = textutils.unserialize(file.readAll())
987		file.close()
988		
989		if type(input) == "table" then
990			if type(input.webMounts) == "table" then configTable.webMounts = input.webMounts end
991			if type(input.githubRepos) == "table" then configTable.githubRepos = input.githubRepos end
992			if type(input.clusters) == "table" then configTable.clusters = input.clusters end
993			if type(input.compressedFS) == "boolean" then configTable.compressedFS = input.compressedFS end
994		end
995	end
996	
997	local webMountList, clusterList, repoList = configTable.webMounts, configTable.clusters, {}
998	for path, url in pairs(configTable.githubRepos) do repoList[path] = getGithubRepo(url) end
999	if next(clusterList) then for _, side in pairs(rs.getSides()) do if peripheral.getType(side) == "modem" then rednet.open(side) end end end
1000	local blacklist = {"bbpack", "bbpack.lua", "startup", "startup.lua", ".settings", ".gif", ".zip", ".bbpack.cfg"}
1001
1002	if not _G.rfs then
1003		local rfs, ramdisk = {}, {}
1004
1005		for key, value in pairs(fs) do rfs[key] = value end
1006		
1007		local function clusterTalk(cluster, answer, ...)
1008			local target, uuid, result, sender, msg = clusterList[cluster], math.random(1, 0x7FFFFFFF), {}
1009
1010			for i = 1, #target do rednet.send(target[i], {["cluster"] = cluster, ["uuid"] = uuid, unpack(arg)}, rednet.host and cluster) end
1011
1012			if answer then
1013				for i = 1, #target do
1014					repeat sender, msg = rednet.receive(rednet.host and cluster) until type(msg) == "table" and msg.cluster == cluster and msg.uuid == uuid
1015					result[i] = msg[1]
1016				end
1017
1018				return result
1019			end
1020		end
1021
1022		_G.fs.list = function(path)
1023			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1024			
1025			path = fs.combine(path, "")
1026			local elements = dividePath(path)
1027
1028			if not fs.isDir(path) then error("Not a directory", 2) end
1029
1030			if fs.getDrive(path) == "hdd" then
1031				local results = rfs.list(path)
1032
1033				for i = 1, #results do
1034					local thisResult = results[i]
1035					if thisResult:sub(-4) == ".bbp" then results[i] = thisResult:sub(1, -5) end
1036				end
1037
1038				for mount in pairs(webMountList) do
1039					local mountElements = dividePath(mount)
1040
1041					if #elements == #mountElements - 1 then
1042						local match = true
1043
1044						for i = 1, #elements do if elements[i] ~= mountElements[i] then
1045							match = false
1046							break
1047						end end
1048
1049						if match then results[#results + 1] = mountElements[#mountElements] end
1050					end
1051				end
1052
1053				if path == "" then
1054					results[#results + 1] = "ram"
1055					for cluster in pairs(clusterList) do results[#results + 1] = cluster end
1056					for repo in pairs(repoList) do results[#results + 1] = repo end
1057				end
1058
1059				table.sort(results)
1060
1061				return results
1062			elseif clusterList[elements[1]] then
1063				local results = {}
1064
1065				local lists = clusterTalk(elements[1], true, "list", path)
1066				for i = 1, #clusterList[elements[1]] do
1067					local subList = lists[i]
1068
1069					for i = 1, #subList do
1070						local found, thisSub = false, subList[i]
1071
1072						for j = 1, #results do if results[j] == thisSub then
1073							found = true
1074							break
1075						end end
1076
1077						if not found then results[#results + 1] = thisSub end
1078					end
1079				end
1080
1081				table.sort(results)
1082
1083				return results
1084			elseif elements[1] == "ram" or repoList[elements[1]] then
1085				local cur, results = (elements[1] == "ram") and ramdisk or repoList[elements[1]], {}
1086
1087				for i = 2, #elements do cur = cur[elements[i]] end
1088
1089				for entry in pairs(cur) do results[#results + 1] = entry end
1090
1091				table.sort(results)
1092
1093				return results
1094			else
1095				return rfs.list(path)
1096			end
1097		end
1098
1099		_G.fs.exists = function(path)
1100			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1101			path = fs.combine(path, "")
1102			local elements = dividePath(path)
1103
1104			if webMountList[path] then
1105				return true
1106			elseif clusterList[elements[1]] then
1107				if #elements == 1 then return true end
1108				local list = clusterTalk(elements[1], true, "exists", path)
1109				for i = 1, #list do if list[i] then return true end end
1110				return false
1111			elseif elements[1] == "ram" or repoList[elements[1]] then
1112				local cur = (elements[1] == "ram") and ramdisk or repoList[elements[1]]
1113
1114				for i = 2, #elements do
1115					cur = cur[elements[i]]
1116					if not cur then return false end
1117				end
1118
1119				return true
1120			else
1121				return rfs.exists(path..".bbp") or rfs.exists(path)
1122			end
1123		end
1124
1125		_G.fs.isDir = function(path)
1126			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1127			if not fs.exists(path) then return false end
1128			path = fs.combine(path, "")
1129			local elements = dividePath(path)
1130
1131			if clusterList[elements[1]] then
1132				if #elements == 1 then return true end
1133				local list = clusterTalk(elements[1], true, "isDir", path)
1134				return list[1]
1135			elseif elements[1] == "ram" or repoList[elements[1]] then
1136				local cur = (elements[1] == "ram") and ramdisk or repoList[elements[1]]
1137
1138				for i = 2, #elements do
1139					cur = cur[elements[i]]
1140				end
1141
1142				return type(cur) == "table"
1143			else
1144				return rfs.isDir(path)
1145			end
1146		end
1147
1148		_G.fs.isReadOnly = function(path)
1149			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1150			path = fs.combine(path, "")
1151			local elements = dividePath(path)
1152
1153			if webMountList[path] or repoList[elements[1]] then
1154				return true
1155			elseif clusterList[elements[1]] or elements[1] == "ram" then
1156				return false
1157			else
1158				return rfs.isReadOnly(rfs.exists(path..".bbp") and (path..".bbp") or path)
1159			end
1160		end
1161
1162		_G.fs.getDrive = function(path)
1163			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1164			path = fs.combine(path, "")
1165			local elements = dividePath(path)
1166
1167			if clusterList[elements[1]] or elements[1] == "ram" or repoList[elements[1]] then
1168				return fs.exists(path) and elements[1]
1169			else
1170				return rfs.getDrive(rfs.exists(path..".bbp") and (path..".bbp") or path)
1171			end
1172		end
1173
1174		_G.fs.getSize = function(path)
1175			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1176			path = fs.combine(path, "")
1177			local elements = dividePath(path)
1178
1179			if webMountList[path] or repoList[elements[1]] then
1180				return 0
1181			elseif clusterList[elements[1]] then
1182				if #elements == 1 then return 0 end
1183				if not fs.exists(path) then error("No such file", 2) end
1184
1185				local size, list = 0, clusterTalk(elements[1], true, "getSize", path)
1186				for i = 1, #clusterList[elements[1]] do size = size + list[i] end
1187				return size
1188			elseif elements[1] == "ram" then
1189				local cur = ramdisk
1190
1191				for i = 2, #elements do
1192					cur = cur[elements[i]]
1193					if not cur then error("No such file", 2) end
1194				end
1195
1196				return type(cur) == "string" and #cur or 0
1197			else
1198				return rfs.getSize(rfs.exists(path..".bbp") and (path..".bbp") or path)
1199			end
1200		end
1201
1202		_G.fs.getFreeSpace = function(path)
1203			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1204			path = fs.combine(path, "")
1205			local elements = dividePath(path)
1206
1207			if clusterList[elements[1]] then
1208				local size, list = 0, clusterTalk(elements[1], true, "getFreeSpace")
1209				for i = 1, #clusterList[elements[1]] do size = size + list[i][1] end
1210				return size
1211			elseif elements[1] == "ram" then
1212				return math.huge
1213			elseif repoList[elements[1]] then
1214				return 0
1215			else
1216				return rfs.getFreeSpace(path)
1217			end
1218		end
1219
1220		_G.fs.makeDir = function(path)
1221			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1222			path = fs.combine(path, "")
1223			local elements = dividePath(path)
1224
1225			if fs.exists(path) then
1226				if fs.isDir(path) then
1227					return
1228				else
1229					error("File exists", 2)
1230				end
1231			end
1232
1233			if clusterList[elements[1]] then
1234				clusterTalk(elements[1], false, "makeDir", path)
1235			elseif elements[1] == "ram" then
1236				local cur = ramdisk
1237
1238				for i = 2, #elements do
1239					local next = cur[elements[i]]
1240
1241					if next then
1242						cur = next
1243					else
1244						cur[elements[i]] = {}
1245						cur = cur[elements[i]]
1246					end
1247				end
1248			elseif repoList[elements[1]] then
1249				error("Access denied", 2)
1250			else
1251				return rfs.makeDir(path)
1252			end
1253		end
1254
1255		_G.fs.move = function(path1, path2)
1256			if type(path1) ~= "string" then error("bad argument #1 (expected string, got " .. type(path1) .. ")", 2 ) end
1257			if type(path2) ~= "string" then error("bad argument #2 (expected string, got " .. type(path2) .. ")", 2 ) end
1258			path1, path2 = fs.combine(path1, ""), fs.combine(path2, "")
1259
1260			if not fs.exists(path1) then error("No such file", 2) end
1261			if fs.exists(path2) then error("File exists", 2) end
1262			if fs.isReadOnly(path1) or fs.isReadOnly(path2) or (#dividePath(path1) == 1 and fs.getDrive(path1) ~= "hdd") then error("Access denied", 2) end
1263			if #dividePath(path1) < #dividePath(path2) and path2:sub(#path1) == path1 then error("Can't copy a directory inside itself", 2) end
1264			-- ... and if we run out of space we'll just let things fall over at the writing level.
1265
1266			if fs.isDir(path1) then
1267				fs.makeDir(path2)
1268				local list = fs.list(path1)
1269				for i = 1, #list do fs.move(fs.combine(path1, list[i]), fs.combine(path2, list[i])) end
1270			else
1271				local input, output = fs.open(path1, "rb"), fs.open(path2, "wb")
1272
1273				if input.readAll then
1274					output.write(input.readAll())
1275				else
1276					for byte in input.read do output.write(byte) end
1277				end
1278
1279				input.close()
1280				output.close()
1281			end
1282
1283			fs.delete(path1)
1284		end
1285
1286		_G.fs.copy = function(path1, path2)
1287			if type(path1) ~= "string" then error("bad argument #1 (expected string, got " .. type(path1) .. ")", 2 ) end
1288			if type(path2) ~= "string" then error("bad argument #2 (expected string, got " .. type(path2) .. ")", 2 ) end
1289			path1, path2 = fs.combine(path1, ""), fs.combine(path2, "")
1290
1291			if not fs.exists(path1) then error("No such file", 2) end
1292			if fs.exists(path2) then error("File exists", 2) end
1293			if fs.isReadOnly(path2) then error("Access denied", 2) end
1294			if #dividePath(path1) < #dividePath(path2) and path2:sub(#path1) == path1 then error("Can't copy a directory inside itself", 2) end
1295			-- ... and if we run out of space we'll just let things fall over at the writing level.
1296
1297			if fs.isDir(path1) then
1298				fs.makeDir(path2)
1299				local list = fs.list(path1)
1300				for i = 1, #list do fs.copy(fs.combine(path1, list[i]), fs.combine(path2, list[i])) end
1301			else
1302				local input, output = fs.open(path1, "rb"), fs.open(path2, "wb")
1303
1304				if input.readAll then
1305					output.write(input.readAll())
1306				else
1307					for byte in input.read do output.write(byte) end
1308				end
1309
1310				input.close()
1311				output.close()
1312			end
1313		end
1314
1315		_G.fs.delete = function(path)
1316			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1317			path = fs.combine(path, "")
1318			local elements = dividePath(path)
1319
1320			if not fs.exists(path) then return end
1321
1322			if webMountList[path] then
1323				webMountList[path] = nil
1324				local file = rfs.open(".bbpack.cfg", "w")
1325				file.write(textutils.serialize(configTable))
1326				file.close()
1327			elseif clusterList[elements[1]] then
1328				if #elements == 1 then
1329					clusterList[elements[1]] = nil
1330					local file = rfs.open(".bbpack.cfg", "w")
1331					file.write(textutils.serialize(configTable))
1332					file.close()
1333				else
1334					clusterTalk(elements[1], false, "delete", path)
1335				end
1336			elseif repoList[elements[1]] then
1337				if #elements == 1 then
1338					repoList[elements[1]], configTable.githubRepos[elements[1]] = nil, nil
1339					local file = rfs.open(".bbpack.cfg", "w")
1340					file.write(textutils.serialize(configTable))
1341					file.close()
1342				else
1343					error("Access denied", 2)
1344				end
1345			elseif elements[1] == "ram" then
1346				if #elements == 1 then error("Access denied", 2) end
1347
1348				local cur = ramdisk
1349
1350				for i = 2, #elements - 1 do
1351					cur = cur[elements[i]]
1352					if not cur then return end
1353				end
1354
1355				cur[elements[#elements]] = nil
1356			else
1357				if fs.isDir(path) then
1358					local list = fs.list(path)
1359					
1360					for i = 1, #list do
1361						local ok, err = pcall(fs.delete, fs.combine(path, list[i]))
1362						if not ok then error(err:gsub("pcall: ", ""), 2) end
1363					end
1364					
1365					return rfs.delete(path)
1366				else
1367					return rfs.delete(rfs.exists(path..".bbp") and (path..".bbp") or path)
1368				end
1369			end
1370		end
1371
1372		_G.fs.open = function(path, mode)
1373			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1374			if type(mode) ~= "string" then error("bad argument #2 (expected string, got " .. type(mode) .. ")", 2 ) end
1375			path = fs.combine(path, "")
1376			local elements = dividePath(path)
1377
1378			mode = mode:lower()
1379			local binary, append, read, write = mode:find("b") ~= nil, mode:find("a") ~= nil, mode:find("r") ~= nil, mode:find("w") ~= nil
1380
1381			if webMountList[path] or clusterList[elements[1]] or elements[1] == "ram" or repoList[elements[1]] then
1382				if not (append or write or read) then error("Invalid file mode: " .. mode, 2) end
1383
1384				if read then
1385					if not fs.exists(path) or fs.isDir(path) then return nil, "No such file" end
1386
1387					local data
1388					local handle = {["close"] = function() data = nil end}
1389
1390					if webMountList[path] then
1391						local webHandle = http.get(webMountList[path], nil, term.setPaletteColour and true)
1392						data = webHandle.readAll()
1393						webHandle.close()
1394					elseif clusterList[elements[1]] then
1395						data = {}
1396						local list = clusterTalk(elements[1], true, "get", path)
1397						for i = 1, #clusterList[elements[1]] do if list[i] then data[list[i][1]] = list[i][2] end end
1398						data = table.concat(data)
1399						data = decompressInternal({data:byte(1, #data)}, true)
1400					elseif repoList[elements[1]] then
1401						data = repoList[elements[1]]
1402						for i = 2, #elements do data = data[elements[i]] end
1403						local webHandle = http.get(data, nil, term.setPaletteColour and true)
1404						data = webHandle.readAll()
1405						webHandle.close()
1406					else
1407						data = ramdisk
1408						for i = 2, #elements do data = data[elements[i]] end
1409					end
1410
1411					if #data == 0 then data = nil end
1412
1413					if binary then
1414						handle.read = function(amount)
1415							if not data then return nil end
1416
1417							local result
1418							if type(amount) ~= "number" then
1419								result = data:byte(1)
1420								data = data:sub(2)
1421							else
1422								result = data:sub(1, amount)
1423								data = data:sub(amount + 1)
1424							end
1425
1426							if #data == 0 then data = nil end
1427							return result
1428						end
1429
1430						handle.readAll = function()
1431							if not data then return nil end
1432
1433							local result = data
1434							data = nil
1435							return result
1436						end
1437					else
1438						handle.readLine = function()
1439							if not data then return nil end
1440
1441							if data:find("\n") then
1442								local result = data:sub(1, data:find("\n") - 1)
1443								data = data:sub(data:find("\n") + 1)
1444								if data:byte(1) == 13 then data = data:sub(2) end
1445								return result
1446							else
1447								local result = data
1448								data = nil
1449								return data
1450							end
1451						end
1452
1453						handle.readAll = function()
1454							if not data then return nil end
1455
1456							local result = data
1457							data = nil
1458							return result
1459						end
1460					end
1461
1462					return handle
1463				elseif write or append then
1464					if webMountList[path] or repoList[elements[1]] then return nil, "Access denied" end
1465					if fs.isDir(path) then return nil, "Cannot write to directory" end
1466					fs.makeDir(fs.getDir(path))
1467
1468					local handle, output, counter = {}, {}, 1
1469
1470					if binary then
1471						handle.write = function(data)
1472							if type(data) ~= "string" and type(data) ~= "number" then error("bad argument #1 (string or number expected, got " .. type(data) .. ")", 2) end
1473							output[counter] = type(data) == "number" and string.char(data) or data
1474							counter = counter + 1
1475						end
1476					else
1477						handle.write = function(data)
1478							output[counter] = tostring(data)
1479							counter = counter + 1
1480						end
1481
1482						handle.writeLine = function(data)
1483							output[counter] = tostring(data)
1484							output[counter + 1] = "\n"
1485							counter = counter + 2
1486						end
1487					end
1488
1489					local ramLink, ramIndex
1490					if clusterList[elements[1]] and append and fs.exists(path) then
1491						local data, list = {}, clusterTalk(elements[1], true, "get", path)
1492						for i = 1, #list do if list[i] then data[list[i][1]] = list[i][2] end end
1493						data = table.concat(data)
1494						output[1], counter = decompressInternal({data:byte(1, #data)}, true), 2
1495					elseif elements[1] == "ram" then
1496						ramLink = ramdisk
1497						for i = 2, #elements - 1 do ramLink = ramLink[elements[i]] end
1498						ramIndex = elements[#elements]
1499						if (append and not ramLink[ramIndex]) or not append then ramLink[ramIndex] = "" end
1500					end
1501
1502					handle.flush = function()
1503						if clusterList[elements[1]] then
1504							output, counter = {table.concat(output)}, 1
1505							local data, segs, pos, totalSpace, thisCluster = string.char(unpack(compressInternal(output[1]))), 1, 1, fs.getFreeSpace(elements[1]), clusterList[elements[1]]
1506							if fs.exists(path) then totalSpace = totalSpace + fs.getSize(path) end
1507							if totalSpace < #data then error("Out of space", 2) end
1508
1509							fs.delete(path)
1510							
1511							local spaceList = clusterTalk(elements[1], true, "getFreeSpace")
1512							
1513							for i = 1, #thisCluster do
1514								local thisSpace = spaceList[i][1]
1515								
1516								if thisSpace > 0 then
1517									local segString = tostring(segs)
1518									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])
1519									pos, segs = pos + thisSpace, segs + 1
1520								end
1521
1522								if pos > #data then break end
1523							end
1524						else
1525							output = table.concat(output)
1526							if append then output = ramLink[ramIndex] .. output end
1527							ramLink[ramIndex] = output
1528							output, counter, append = {}, 1, true
1529						end
1530					end
1531
1532					handle.close = function()
1533						handle.flush()
1534						for key in pairs(handle) do handle[key] = function() end end
1535					end
1536
1537					return handle
1538				end
1539			else
1540				if (write or append) and rfs.isReadOnly(path) then return nil, "Access denied" end
1541
1542				for i = 1, #blacklist do
1543					local check = blacklist[i]
1544					if path:sub(-#check):lower() == check then return rfs.open(path, mode) end
1545				end
1546
1547				if read then
1548					return rfs.exists(path .. ".bbp") and open(path .. ".bbp", mode) or rfs.open(path, mode)
1549				elseif configTable.compressedFS then
1550					if rfs.getDrive(elements[1]) and rfs.getDrive(elements[1]) ~= "hdd" then
1551						return rfs.open(path, mode)
1552					elseif append then
1553						if rfs.exists(path) then
1554							local file, content = rfs.open(path, binary and "rb" or "r")
1555
1556							if file.readAll then
1557								content = file.readAll()
1558							else
1559								content = {}
1560								for byte in file.read do content[#content + 1] = byte end
1561								content = string.char(unpack(content))
1562							end
1563
1564							file.close()
1565
1566							rfs.delete(path)
1567
1568							file = open(path .. ".bbp", binary and "wb" or "w")
1569							file.write(content)
1570							return file
1571						else return open(path .. ".bbp", mode) end
1572					elseif write then
1573						if rfs.exists(path) then rfs.delete(path) end
1574						return open(path .. ".bbp", mode)
1575					end
1576				else
1577					if append then
1578						if rfs.exists(path .. ".bbp") then
1579							local file = open(path .. ".bbp", binary and "rb" or "r")
1580							local content = file.readAll()
1581							file.close()
1582
1583							rfs.delete(path .. ".bbp")
1584
1585							file = rfs.open(path, binary and "wb" or "w")
1586
1587							if file.writeLine or term.setPaletteColour then
1588								file.write(content)
1589							else
1590								content = {string.byte(1, #content)}
1591								for i = 1, #content do file.write(content[i]) end
1592							end
1593
1594							return file
1595						else return rfs.open(path, mode) end
1596					elseif write then
1597						if rfs.exists(path .. ".bbp") then rfs.delete(path .. ".bbp") end
1598						return rfs.open(path, mode)
1599					end
1600				end
1601
1602				error("Unsupported mode", 2)
1603			end
1604		end
1605
1606		_G.fs.find = function(path)
1607			if type(path) ~= "string" then error("bad argument #1 (expected string, got " .. type(path) .. ")", 2 ) end
1608			local pathParts, results, curfolder = {}, {}, "/"
1609			for part in path:gmatch("[^/]+") do pathParts[#pathParts + 1] = part:gsub("*", "[^/]*") end
1610			if #pathParts == 0 then return {} end
1611
1612			local prospects = fs.list(curfolder)
1613			for i = 1, #prospects do prospects[i] = {["parent"] = curfolder, ["depth"] = 1, ["name"] = prospects[i]} end
1614
1615			while #prospects > 0 do
1616				local thisProspect = table.remove(prospects, 1)
1617				local fullPath = fs.combine(thisProspect.parent, thisProspect.name)
1618
1619				if thisProspect.name == thisProspect.name:match(pathParts[thisProspect.depth]) then
1620					if thisProspect.depth == #pathParts then
1621						results[#results + 1] = fullPath
1622					elseif fs.isDir(fullPath) and thisProspect.depth < #pathParts then
1623						local newList = fs.list(fullPath)
1624						for i = 1, #newList do prospects[#prospects + 1] = {["parent"] = fullPath, ["depth"] = thisProspect.depth + 1, ["name"] = newList[i]} end
1625					end
1626				end
1627			end
1628
1629			return results
1630		end
1631
1632		_G.rfs = rfs
1633	end
1634
1635	update = bbpack and bbpack.update or function()
1636		for cluster, ids in pairs(clusterList) do for i = 1, #ids do
1637			rednet.send(ids[i], {["cluster"] = cluster, "update"}, rednet.host and cluster)
1638		end end
1639
1640		local file = rfs.open("bbpack", "w")
1641		file.write(downloadPasteInternal("cUYTGbpb"))
1642		file.close()
1643
1644		os.reboot()
1645	end
1646
1647	fileSys = bbpack and bbpack.fileSys or function(par1, par2)
1648		if type(par1) == "boolean" or (type(par1) == "string" and type(par2) == "boolean") then
1649			-- Compress / decompress hdd contents.
1650			local list
1651			if type(par1) == "boolean" then
1652				list = rfs.list("")
1653				configTable.compressedFS = par1
1654			else
1655				list = {par1}
1656				par1 = par2
1657			end
1658
1659			while #list > 0 do
1660				local entry = list[#list]
1661				list[#list] = nil
1662
1663				if rfs.getDrive(entry) == "hdd" and not webMountList[entry] then
1664					if rfs.isDir(entry) then
1665						local newList, curLen = rfs.list(entry), #list
1666						for i = 1, #newList do list[curLen + i] = fs.combine(entry, newList[i]) end
1667					else
1668						local blacklisted = false
1669
1670						for i = 1, #blacklist do
1671							local check = blacklist[i]
1672							if entry:sub(-#check):lower() == check then
1673								blacklisted = true
1674								break
1675							end
1676						end
1677
1678						if not blacklisted then
1679							if par1 and entry:sub(-4) ~= ".bbp" then
1680								-- Compress this file.
1681								local file, content = rfs.open(entry, "rb")
1682								if file.readAll then
1683									content = file.readAll()
1684								else
1685									content = {}
1686									local counter = 1
1687									for byte in file.read do
1688										content[counter] = byte
1689										counter = counter + 1
1690									end
1691									content = string.char(unpack(content))
1692								end
1693								file.close()
1694								
1695								content = compressInternal(content)
1696								if rfs.getFreeSpace(entry) + rfs.getSize(entry) < #content then return false end
1697								rfs.delete(entry)
1698								snooze()
1699
1700								file = rfs.open(entry .. ".bbp", "wb")
1701
1702								if term.setPaletteColor then
1703									file.write(string.char(unpack(content)))
1704								else
1705									for i = 1, #content do file.write(content[i]) end
1706								end
1707
1708								file.close()
1709
1710								snooze()
1711							elseif not par1 and entry:sub(-4) == ".bbp" then
1712								-- Decompress this file.
1713								local file = open(entry, "rb")
1714								local content = file.readAll()
1715								file.close()
1716
1717								if rfs.getFreeSpace(entry) + rfs.getSize(entry) < #content then return false end
1718								rfs.delete(entry)
1719								snooze()
1720
1721								file = rfs.open(entry:sub(1, -5), "wb")
1722
1723								if term.setPaletteColor then
1724									file.write(content)
1725								else
1726									content = {content:byte(1, #content)}
1727									for i = 1, #content do file.write(content[i]) end
1728								end
1729
1730								file.close()
1731
1732								snooze()
1733							end
1734						end
1735					end
1736				end
1737			end
1738		elseif type(par1) == "string" and type(par2) == "string" then
1739			-- New web mount.
1740			local url, path = par1, fs.combine(par2, "")
1741			local elements = dividePath(path)
1742			
1743			local repo = getGithubRepo(url)
1744			if repo then
1745				if #elements > 1 then error("bbpack.mount: Github repos must be mounted at the root of your file system", 2) end
1746				repoList[path] = repo
1747				configTable.githubRepos[path] = url
1748			else
1749				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
1750
1751				local get = http.get(url)
1752				if not get then error("bbpack.mount: Can't connect to URL: "..url, 2) end
1753				get.close()
1754
1755				webMountList[path] = url
1756			end
1757		elseif type(par1) == "string" then
1758			-- New cluster mount.
1759			local cluster, uuid = par1, math.random(1, 0x7FFFFFFF)
1760			for _, side in pairs(rs.getSides()) do if peripheral.getType(side) == "modem" then rednet.open(side) end end
1761			rednet.broadcast({["cluster"] = cluster, ["uuid"] = uuid, "rollcall"}, rednet.host and cluster)
1762			clusterList[cluster] = nil
1763			local myTimer, map = os.startTimer(5), {}
1764
1765			while true do
1766				local event, par1, par2 = os.pullEvent()
1767
1768				if event == "timer" and par1 == myTimer then
1769					break
1770				elseif event == "rednet_message" and type(par2) == "table" and par2.cluster == cluster and par2.uuid == uuid and par2[1] == "rollcallResponse" then
1771					map[#map + 1] = par1
1772				end
1773			end
1774
1775			if #map == 0 then error("bbpack.mount: Can't connect to cluster: " .. cluster, 2) end
1776			clusterList[cluster] = map
1777		end
1778
1779		local file = rfs.open(".bbpack.cfg", "w")
1780		file.write(textutils.serialize(configTable))
1781		file.close()
1782
1783		return true
1784	end
1785end