· 4 years ago · Jun 14, 2021, 04:12 PM
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