· 9 years ago · Feb 02, 2017, 03:48 PM
1home/bin/tar.lua0000600000175000017500000006522212562645047011001 0ustar -----------------------------------------------------
2--name : bin/tar.lua
3--description: creating, viewing and extracting tar archives on disk and tape
4--author : mpmxyz
5--github page: https://github.com/mpmxyz/ocprograms
6--forum page : http://oc.cil.li/index.php?/topic/421-tar-for-opencomputers/
7-----------------------------------------------------
8--[[
9 tar archiver for OpenComputers
10 for further information check the usage text or man page
11
12 TODO: support non primary tape drives
13 TODO: detect symbolic link cycles (-> remember already visited, resolved paths)
14]]
15local shell = require 'shell'
16local fs = require 'filesystem'
17local component = require 'component'
18
19local BLOCK_SIZE = 512
20local NULL_BLOCK = ("\0"):rep(BLOCK_SIZE)
21local WORKING_DIRECTORY = fs.canonical(shell.getWorkingDirectory()):gsub("/$","")
22
23--load auto_progress library if possible
24local auto_progress
25if true then
26 local ok
27 ok, auto_progress = pcall(require, "mpm.auto_progress")
28 if not ok then
29 auto_progress = {}
30 function auto_progress.new()
31 --library not available, create stub
32 return {
33 update = function() end,
34 finish = function() end,
35 }
36 end
37 end
38end
39
40--error information
41local USAGE_TEXT = [[
42Usage:
43tar <function letter> [other options] FILES...
44function letter description
45-c --create creates a new archive
46-r --append appends to existing archive
47-t --list lists contents from archive
48-x --extract --get extracts from archive
49other options description
50-f --file FILE first FILE is archive, else:
51 uses primary tape drive
52-h --dereference follows symlinks
53--exclude=FILE;... excludes FILE from archive
54-v --verbose lists processed files, also
55 shows progress for large files
56]]
57local function addUsage(text)
58 return text .. "\n" .. USAGE_TEXT
59end
60local ERRORS = {
61 missingAction = addUsage("Error: Missing function letter!"),
62 multipleActions = addUsage("Error: Multiple function letters!"),
63 missingFiles = addUsage("Error: Missing file names!"),
64 invalidChecksum = "Error: Invalid checksum!",
65 noHeaderName = "Error: No file name in header!",
66 invalidTarget = "Error: Invalid target!",
67}
68
69
70--formats numbers and stringvalues to comply to the tar format
71local function formatValue(text, length, maxDigits)
72 if type(text) == "number" then
73 maxDigits = maxDigits or (length - 1) --that is default
74 text = ("%0"..maxDigits.."o"):format(text):sub(-maxDigits, -1)
75 elseif text == nil then
76 text = ""
77 end
78 return (text .. ("\0"):rep(length - #text)):sub(-length, -1)
79end
80
81--a utility to make accessing the header easier
82--Only one header is accessed at a time: no need to throw tables around.
83local header = {}
84--loads a header, uses table.concat on tables, strings are taken directly
85function header:init(block)
86 if type(block) == "table" then
87 --combine tokens to form one 512 byte long header
88 block = table.concat(block, "")
89 elseif block == nil then
90 --make this header a null header
91 block = NULL_BLOCK
92 end
93 if #block < BLOCK_SIZE then
94 --add "\0"s to reach 512 bytes
95 block = block .. ("\0"):rep(BLOCK_SIZE - #block)
96 end
97 --remember the current block
98 self.block = block
99end
100--takes the given data and creates a header from it
101--the resulting block can be retrieved via header:getBytes()
102function header:assemble(data)
103 if #data.name > 100 then
104 local longName = data.name
105 --split at slash
106 local minPrefixLength = #longName - 101 --(100x suffix + 1x slash)
107 local splittingSlashIndex = longName:find("/", minPrefixLength + 1, true)
108 if splittingSlashIndex then
109 --can split path in 2 parts separated by a slash
110 data.filePrefix = longName:sub(1, splittingSlashIndex - 1)
111 data.name = longName:sub(splittingSlashIndex + 1, -1)
112 else
113 --unable to split path; try to put path to the file prefix
114 data.filePrefix = longName
115 data.name = ""
116 end
117 --checking for maximum file prefix length
118 assert(#data.filePrefix <= 155, "File name '"..longName.."' is too long; unable to apply ustar splitting!")
119 --force ustar format
120 data.ustarIndicator = "ustar"
121 data.ustarVersion = "00"
122 end
123 local tokens = {
124 formatValue(data.name, 100), --1
125 formatValue(data.mode, 8), --2
126 formatValue(data.owner, 8), --3
127 formatValue(data.group, 8), --4
128 formatValue(data.size, 12), --5
129 formatValue(data.lastModified, 12), --6
130 " ",--8 spaces --7
131 formatValue(data.typeFlag, 1), --8
132 formatValue(data.linkName, 100), --9
133 }
134 --ustar extension?
135 if data.ustarIndicator then
136 table.insert(tokens, formatValue(data.ustarindicator, 6))
137 table.insert(tokens, formatValue(data.ustarversion, 2))
138 table.insert(tokens, formatValue(data.ownerUser, 32))
139 table.insert(tokens, formatValue(data.ownerGroup, 32))
140 table.insert(tokens, formatValue(data.deviceMajor, 8))
141 table.insert(tokens, formatValue(data.deviceMinor, 8))
142 table.insert(tokens, formatValue(data.filePrefix, 155))
143 end
144 --temporarily assemble header for checksum calculation
145 header:init(tokens)
146 --calculating checksum
147 tokens[7] = ("%06o\0\0"):format(header:checksum(0, BLOCK_SIZE))
148 --assemble final header
149 header:init(tokens)
150end
151--extracts the information from the given header
152function header:read()
153 local data = {}
154 data.name = self:extract (0 , 100)
155 data.mode = self:extract (100, 8)
156 data.owner = self:extractNumber(108, 8)
157 data.group = self:extractNumber(116, 8)
158 data.size = self:extractNumber(124, 12)
159 data.lastModified = self:extractNumber(136, 12)
160 data.checksum = self:extractNumber(148, 8)
161 data.typeFlag = self:extract (156, 1) or "0"
162 data.linkName = self:extract (157, 100)
163 data.ustarIndicator = self:extract (257, 6)
164
165 --There is an old format using "ustar \0" instead of "ustar\0".."00"?
166 if data.ustarIndicator and data.ustarIndicator:sub(1,5) == "ustar" then
167 data.ustarVersion = self:extractNumber(263, 2)
168 data.ownerUser = self:extract (265, 32)
169 data.ownerGroup = self:extract (297, 32)
170 data.deviceMajor = self:extractNumber(329, 8)
171 data.deviceMinor = self:extractNumber(337, 8)
172 data.filePrefix = self:extract (345, 155)
173 end
174
175 assert(self:verify(data.checksum), ERRORS.invalidChecksum)
176 --assemble raw file name, normally relative to working dir
177 if data.filePrefix then
178 data.name = data.filePrefix .. "/" .. data.name
179 data.filePrefix = nil
180 end
181 assert(data.name, ERRORS.noHeaderName)
182 return data
183end
184--returns the whole 512 bytes of the header
185function header:getBytes()
186 return header.block
187end
188--returns if the header is a null header
189function header:isNull()
190 return self.block == NULL_BLOCK
191end
192--extracts a 0 terminated string from the given area
193function header:extract(offset, size)
194 --extract size bytes from the given offset, strips every NULL character
195 --returns a string
196 return self.block:sub(1 + offset, size + offset):match("[^\0]+")
197end
198--extracts an octal number from the given area
199function header:extractNumber(offset, size)
200 --extract size bytes from the given offset
201 --returns the first series of octal digits converted to a number
202 return tonumber(self.block:sub(1 + offset, size + offset):match("[0-7]+") or "", 8)
203end
204--calculates the checksum for the given area
205function header:checksum(offset, size, signed)
206 --calculates the checksum of a given range
207 local sum = 0
208 --summarize byte for byte
209 for index = 1 + offset, size + offset do
210 if signed then
211 --interpretation of a signed byte: compatibility for bugged implementations
212 sum = sum + (self.block:byte(index) + 128) % 256 - 128
213 else
214 sum = sum + self.block:byte(index)
215 end
216 end
217 --modulo to take care of negative sums
218 --The whole reason for the signed addition is that some implementations
219 --used signed bytes instead of unsigned ones and therefore computed 'wrong' checksums.
220 return sum % 0x40000
221end
222--checks if the given checksum is valid for the loaded header
223function header:verify(checksum)
224 local checkedSums = {
225 [self:checksum(0, 148, false) + 256 + self:checksum(156, 356, false)] = true,
226 [self:checksum(0, 148, true ) + 256 + self:checksum(156, 356, true )] = true,
227 }
228 return checkedSums[checksum] or false
229end
230
231
232local function makeRelative(path, reference)
233 --The path and the reference directory must have a common reference. (e.g. root)
234 --The default reference is the current working directory.
235 reference = reference or WORKING_DIRECTORY
236 --1st: split paths into segments
237 local returnDirectory = path:sub(-1,-1) == "/" --?
238 path = fs.segments(path)
239 reference = fs.segments(reference)
240 --2nd: remove common directories
241 while path[1] and reference[1] and path[1] == reference[1] do
242 table.remove(path, 1)
243 table.remove(reference, 1)
244 end
245 --3rd: add ".."s to leave that what's left of the working directory
246 local path = ("../"):rep(#reference) .. table.concat(path, "/")
247 --4th: If there is nothing remaining, we are at the current directory.
248 if path == "" then
249 path = "."
250 end
251 return path
252end
253
254
255local function tarFiles(files, options, mode, ignoredObjects, isDirectoryContent)
256 --combines files[2], files[3], ... into files[1]
257 --prepare output stream
258 local target, closeAtExit
259 if type(files[1]) == "string" then
260 --mode = append -> overwrite trailing NULL headers
261 local targetFile = shell.resolve(files[1]):gsub("/$","")
262 ignoredObjects[targetFile] = ignoredObjects[targetFile] or true
263 target = assert(io.open(targetFile, mode))
264 closeAtExit = true
265 else
266 target = files[1]
267 closeAtExit = false
268 assert(target.write, ERRORS.invalidTarget)
269 end
270 if mode == "rb+" then --append: not working with files because io.read does not support mode "rb+"
271 --start from beginning of file
272 assert(target:seek("set", 0))
273 --loop over every block
274 --This loop implies that it is okay if there is nothing (!) after the last file block.
275 --It also ensures that trailing null blocks are overwritten.
276 for block in target:lines(BLOCK_SIZE) do
277 if #block < BLOCK_SIZE then
278 --reached end of file before block was finished
279 error("Missing "..(BLOCK_SIZE-#block).." bytes to finish block.")
280 end
281 --load header
282 header:init(block)
283 if header:isNull() then
284 --go back to the beginning of the block
285 assert(target:seek("cur", -BLOCK_SIZE))
286 --found null header -> finished with skipping
287 break
288 end
289 --extract size information from header
290 local data = header.read(header)
291 if data.size and data.size > 0 then
292 --skip file content
293 local skippedBytes = math.ceil(data.size / BLOCK_SIZE) * BLOCK_SIZE
294 assert(target:seek("cur", skippedBytes))
295 end
296 end
297 if options.verbose then
298 print("End of archive detected; appending...")
299 end
300 end
301 for i = 2, #files do
302 --prepare data
303 --remove trailing slashes that might come from fs.list
304 local file = shell.resolve(files[i]):gsub("/$","")
305 --determine object type, that determines how the object is handled
306 local isALink, linkTarget = fs.isLink(file)
307 local objectType
308 if isALink and not options.dereference then
309 objectType = "link" --It's a symbolic link.
310 else
311 if fs.isDirectory(file) then
312 objectType = "dir" --It's a directory.
313 else
314 objectType = "file" --It's a normal file.
315 end
316 end
317 --add directory contents before the directory
318 --(It makes sense if you consider that you could change the directories file permissions to be read only.)
319 if objectType == "dir" and ignoredObjects[file] ~= "strict" then
320 local list = {target}
321 local i = 2
322 for containedFile in fs.list(file) do
323 list[i] = fs.concat(file, containedFile)
324 i = i + 1
325 end
326 tarFiles(list, options, nil, ignoredObjects, true)
327 end
328 --Ignored objects are not added to the tar.
329 if not ignoredObjects[file] then
330 local data = {}
331 --get relative path to current directory
332 data.name = makeRelative(file)
333 --add object specific data
334 if objectType == "link" then
335 --It's a symbolic link.
336 data.typeFlag = "2"
337 data.linkName = makeRelative(linkTarget, fs.path(file)):gsub("/$","") --force relative links
338 else
339 data.lastModified = math.floor(fs.lastModified(file) / 1000) --Java returns milliseconds...
340 if objectType == "dir" then
341 --It's a directory.
342 data.typeFlag = "5"
343 data.mode = 448 --> 700 in octal -> rwx------
344 elseif objectType == "file" then
345 --It's a normal file.
346 data.typeFlag = "0"
347 data.size = fs.size(file)
348 data.mode = 384 --> 600 in octal -> rw-------
349 end
350 end
351
352 --tell user what is going on
353 if options.verbose then
354 print("Adding:", data.name)
355 end
356 --assemble header
357 header:assemble(data)
358 --write header
359 assert(target:write(header:getBytes()))
360 --copy file contents
361 if objectType == "file" then
362 --open source file
363 local source = assert(io.open(file, "rb"))
364 --keep track of what has to be copied
365 local bytesToCopy = data.size
366 --init progress bar
367 local progressBar = auto_progress.new(bytesToCopy)
368 --copy file contents
369 for block in source:lines(BLOCK_SIZE) do
370 assert(target:write(block))
371 bytesToCopy = bytesToCopy - #block
372 assert(bytesToCopy >= 0, "Error: File grew while copying! Is it the output file?")
373 if options.verbose then
374 --update progress bar
375 progressBar.update(#block)
376 end
377 if #block < BLOCK_SIZE then
378 assert(target:write(("\0"):rep(BLOCK_SIZE - #block)))
379 break
380 end
381 end
382 --close source file
383 source:close()
384 if options.verbose then
385 --draw full progress bar
386 progressBar.finish()
387 end
388 assert(bytesToCopy <= 0, "Error: Could not copy file!")
389 end
390 end
391 end
392 if not isDirectoryContent then
393 assert(target:write(NULL_BLOCK)) --Why wasting 0.5 KiB if you can waste a full KiB? xD
394 assert(target:write(NULL_BLOCK)) --(But that's the standard!)
395 end
396 if closeAtExit then
397 target:close()
398 end
399end
400
401
402
403local extractingExtractors = {
404 ["0"] = function(data, options) --file
405 --creates a file at data.file and fills it with data.size bytes
406 --ensure that the directory is existing
407 local dir = fs.path(data.file)
408 if not fs.exists(dir) then
409 fs.makeDirectory(dir)
410 end
411 --don't overwrite the file if true
412 local skip = false
413 --check for existing file
414 if fs.exists(data.file) then
415 if options.verbose then
416 print("File already exists!")
417 end
418 --check for options specifying what to do now...
419 if options["keep-old-files"] then
420 error("Error: Attempting to overwrite: '"..data.file.."'!")
421 elseif options["skip-old-files"] then
422 --don't overwrite
423 skip = true
424 elseif options["keep-newer-files"] and data.lastModified then
425 --don't overwrite when file on storage is newer
426 local lastModifiedOnDrive = math.floor(fs.lastModified(data.file) / 1000)
427 if lastModifiedOnDrive > data.lastModified then
428 skip = true
429 end
430 else
431 --default: overwrite
432 end
433 if options.verbose and not skip then
434 --verbose: tell user that we are overwriting
435 print("Overwriting...")
436 end
437 end
438 if skip then
439 --go to next header
440 return data.size
441 end
442
443 --open target file
444 local target = assert(io.open(data.file, "wb"))
445 --set file length
446 local bytesToCopy = data.size
447 --init progress bar
448 local progressBar = auto_progress.new(bytesToCopy)
449 --create extractor function, writes min(bytesToCopy, #block) bytes to target
450 local function extractor(block)
451 --shortcut for abortion
452 if block == nil then
453 target:close()
454 return nil
455 end
456 --adjust block size to missing number of bytes
457 if #block > bytesToCopy then
458 block = block:sub(1, bytesToCopy)
459 end
460 --write up to BLOCK_SIZE bytes
461 assert(target:write(block))
462 --subtract copied amount of bytes from bytesToCopy
463 bytesToCopy = bytesToCopy - #block
464 if bytesToCopy <= 0 then
465 --close target stream when done
466 target:close()
467 if options.verbose then
468 --draw full progress bar
469 progressBar.finish()
470 end
471 --return nil to finish
472 return nil
473 else
474 if options.verbose then
475 --update progress bar
476 progressBar.update(#block)
477 end
478 --continue
479 return extractor
480 end
481 end
482 if bytesToCopy > 0 then
483 return extractor
484 else
485 target:close()
486 end
487 end,
488 ["2"] = function(data, options) --symlink
489 --ensure that the directory is existing
490 local dir = fs.path(data.file)
491 if not fs.exists(dir) then
492 fs.makeDirectory(dir)
493 end
494 --check for existing file
495 if fs.exists(data.file) then
496 if options.verbose then
497 print("File already exists!")
498 end
499 if options["keep-old-files"] then
500 error("Error: Attempting to overwrite: '"..data.file.."'!")
501 elseif options["skip-old-files"] then
502 return
503 elseif options["keep-newer-files"] and data.lastModified then
504 --don't overwrite when file on storage is newer
505 local lastModifiedOnDrive = math.floor(fs.lastModified(data.file) / 1000)
506 if lastModifiedOnDrive > data.lastModified then
507 return
508 end
509 else
510 --default: overwrite file
511 end
512 --delete original file
513 if options.verbose then
514 print("Overwriting...")
515 end
516 assert(fs.remove(data.file))
517 end
518 assert(fs.link(data.linkName, data.file))
519 end,
520 ["5"] = function(data, options) --directory
521 if not fs.isDirectory(data.file) then
522 assert(fs.makeDirectory(data.file))
523 end
524 end,
525}
526local listingExtractors = {
527 ["0"] = function(data, options) --file
528 --output info
529 print("File:", data.name)
530 print("Size:", data.size)
531 --go to next header
532 return data.size
533 end,
534 ["1"] = function(data, options) --hard link: unsupported, but reported
535 print("Hard link (unsupported):", data.name)
536 print("Target:", data.linkName)
537 end,
538 ["2"] = function(data, options) --symlink
539 print("Symbolic link:", data.name)
540 print("Target:", data.linkName)
541 end,
542 ["3"] = function(data, options) --device file: unsupported, but reported
543 print("Device File (unsupported):", data.name)
544 end,
545 ["4"] = function(data, options) --device file: unsupported, but reported
546 print("Device File (unsupported):", data.name)
547 end,
548 ["5"] = function(data, options) --directory
549 print("Directory:", data.name)
550 end,
551}
552
553
554local function untarFiles(files, options, extractorList)
555 --extracts the contents of every tar file given
556 for _,file in ipairs(files) do
557 --prepare input stream
558 local source, closeAtExit
559 if type(file) == "string" then
560 source = assert(io.open(shell.resolve(file), "rb"))
561 closeAtExit = true
562 else
563 source = file
564 closeAtExit = false
565 assert(source.lines, "Unknown source type.")
566 end
567 local extractor = nil
568 local hasDoubleNull = false
569 for block in source:lines(BLOCK_SIZE) do
570 if #block < BLOCK_SIZE then
571 error("Error: Unfinished Block; missing "..(BLOCK_SIZE-#block).." bytes!")
572 end
573 if extractor == nil then
574 --load header
575 header:init(block)
576 if header:isNull() then
577 --check for second null block
578 if source:read(BLOCK_SIZE) == NULL_BLOCK then
579 hasDoubleNull = true
580 end
581 --exit/close file when there is a NULL header
582 break
583 else
584 --read block as header
585 local data = header:read()
586 if options.verbose then
587 --tell user what is going on
588 print("Extracting:", data.name)
589 end
590 --enforcing relative paths
591 data.file = shell.resolve(WORKING_DIRECTORY.."/"..data.name)
592 --get extractor
593 local extractorInit = extractorList[data.typeFlag]
594 assert(extractorInit, "Unknown type flag \""..tostring(data.typeFlag).."\"")
595 extractor = extractorInit(data, options)
596 end
597 else
598 extractor = extractor(block)
599 end
600 if type(extractor) == "number" then
601 if extractor > 0 then
602 --adjust extractorInit to block size
603 local bytesToSkip = math.ceil(extractor / BLOCK_SIZE) * BLOCK_SIZE
604 --skip (extractorInit) bytes
605 assert(source:seek("cur", bytesToSkip))
606 end
607 --expect next header
608 extractor = nil
609 end
610 end
611 assert(extractor == nil, "Error: Reached end of file but expecting more data!")
612 if closeAtExit then
613 source:close()
614 end
615 if not hasDoubleNull then
616 print("Warning: Archive does not end with two Null blocks!")
617 end
618 end
619end
620
621
622--connect function parameters with actions
623local actions = {
624 c = function(files, options, ignoredObjects) --create
625 tarFiles(files, options, "wb", ignoredObjects)
626 end,
627 r = function(files, options, ignoredObjects) --append
628 tarFiles(files, options, "rb+", ignoredObjects)
629 end,
630 x = function(files, options, ignoredObjects) --extract
631 untarFiles(files, options, extractingExtractors)
632 end,
633 t = function(files, options, ignoredObjects) --list
634 untarFiles(files, options, listingExtractors)
635 end,
636}
637--also add some aliases
638actions["create"] = actions.c
639actions["append"] = actions.r
640actions["list"] = actions.t
641actions["extract"] = actions.x
642actions["get"] = actions.x
643
644local debugEnabled = false
645
646local function main(...)
647 --variables containing the processed arguments
648 local action, files
649 --prepare arguments
650 local params, options = shell.parse(...)
651 --add stacktrace to output
652 debugEnabled = options.debug
653 --quick help
654 if options.help then
655 print(USAGE_TEXT)
656 return
657 end
658 --determine executed function and options
659 for option, value in pairs(options) do
660 local isAction = actions[option]
661 if isAction then
662 assert(action == nil, ERRORS.multipleActions)
663 action = isAction
664 options[option] = nil
665 end
666 end
667 assert(action ~= nil, ERRORS.missingAction)
668 --prepare file names
669 files = params
670 --process options
671 if options.v then
672 options.verbose = true
673 end
674 if options.dir then
675 assert(options.dir ~= true and options.dir ~= "", "Error: Invalid --dir value!")
676 WORKING_DIRECTORY = shell.resolve(options.dir) or options.dir
677 assert(WORKING_DIRECTORY ~= nil and WORKING_DIRECTORY ~= "", "Error: Invalid --dir value!")
678 end
679 if options.f or options.file then
680 --use file for archiving
681 --keep file names as they are
682 else
683 --use tape
684 local tapeFile = {drive = component.tape_drive, pos = 0}
685 do
686 --check for Computronics bug
687 local endInversionBug = false
688 --step 1: move to the end of the tape
689 local movedBy = assert(tapeFile.drive.seek(tapeFile.drive.getSize()))
690 --step 2: check output of isEnd
691 if tapeFile.drive.isEnd() ~= true then
692 endInversionBug = true
693 end
694 --step 3: restore previous position
695 assert(tapeFile.drive.seek(-movedBy) == -movedBy, "Error: Tape did not return to original position after checking for isEnd bug!")
696
697 if endInversionBug then
698 if options.verbose then
699 print("tape_drive.isEnd() bug detected; adjusting...")
700 end
701 function tapeFile:isEnd()
702 --This is a workaround for bugged versions of Computronics.
703 return (not self.drive.isEnd()) or (not self.drive.isReady())
704 end
705 else
706 function tapeFile:isEnd()
707 --This does not work in bugged versions of Computronics.
708 return self.drive.isEnd()
709 end
710 end
711 end
712 --create some kind of "tape stream" with limited buf sufficient functionality
713 function tapeFile:lines(byteCount)
714 return function()
715 return self:read(byteCount)
716 end
717 end
718 function tapeFile:read(byteCount)
719 if self:isEnd() then
720 return nil
721 end
722 local data = self.drive.read(byteCount)
723 self.pos = self.pos + #data
724 return data
725 end
726 function tapeFile:write(text)
727 self.drive.write(text)
728 self.pos = self.pos + #text
729 if self:isEnd() then
730 return nil, "Error: Reached end of tape!"
731 else
732 return self
733 end
734 end
735 function tapeFile:seek(typ, pos)
736 local toSeek
737 if typ == "set" then
738 toSeek = pos - self.pos
739 elseif typ == "cur" then
740 toSeek = pos
741 end
742 local movedBy = 0
743 if toSeek ~= 0 then
744 movedBy = self.drive.seek(toSeek)
745 self.pos = self.pos + movedBy
746 end
747 if movedBy == toSeek then
748 return self.pos
749 else
750 return nil, "Error: Unable to seek!"
751 end
752 end
753 --add tape before first file
754 table.insert(files, 1, tapeFile)
755 end
756 if options.h then
757 options.dereference = true
758 end
759
760 --prepare list of ignored objects, default is the current directory and the target file if applicable
761 local ignoredObjects = {}
762 ignoredObjects[WORKING_DIRECTORY] = true
763 if options.exclude then
764 --";" is used as a separator
765 for excluded in options.exclude:gmatch("[^%;]+") do
766 ignoredObjects[shell.resolve(excluded) or excluded] = "strict"
767 end
768 end
769
770 assert(#files > 0, ERRORS.missingFiles)
771 --And action!
772 action(files, options, ignoredObjects)
773end
774
775--adding stack trace when --debug is used
776local function errorFormatter(msg)
777 msg = msg:gsub("^[^%:]+%:[^%:]+%: ","")
778 if debugEnabled then
779 --add traceback when debugging
780 return debug.traceback(msg, 3)
781 end
782 return msg
783end
784
785local ok, msg = xpcall(main, errorFormatter, ...)
786if not ok then
787 io.stdout:write(msg)
788end
789home/lib/mpm/auto_progress.lua0000600000175000017500000002014312562645050013661 0ustar -----------------------------------------------------
790--name : lib/mpm/auto_progress.lua
791--description: display progress bars while your program is doing something
792--author : mpmxyz
793--github page: https://github.com/mpmxyz/ocprograms
794--forum page : http://oc.cil.li/index.php?/topic/420-mpmauto-progress-a-console-progress-bar-for-impatient-users/
795-----------------------------------------------------
796
797--[[
798 auto_progress
799 allows you to create an object that displays a nice progress bar when things take a long time
800 example code:
801
802--load library
803local auto_progress = require 'mpm.auto_progress'
804--generate a list of actions
805local todo = {}
806for i=1, 30 do
807 todo[i] = math.random() * 2
808end
809--create progress bar: initialize it with the amount of work to do
810local pbar = auto_progress.new(#todo)
811for _, duration in pairs(todo) do
812 --simulate an action
813 os.sleep(duration)
814 --update progress bar: 1 step done
815 pbar.update(1)
816end
817--tell progress bar that the action has been finished
818pbar.finish()
819]]
820
821local component = require("component")
822local computer = require("computer")
823local term = require("term")
824
825--CONSTANTS--
826--the time that has to pass before the progress bar is first displayed
827local DRAWING_DELAY = 1
828--do not trigger drawing if the progress bar is above this percentage before TRIGGER_TIME is finished
829--avoids 'last minute' triggering of operations that are almost finished
830local INHIBITING_PROGRESS = 0.5 --0.5 = 50%
831--the time between two drawing operations (limits how often the progress bar is updated)
832local REFRESH_TIME = 0.25
833
834--TIME -> STRING--
835local timeUnits = {
836--format string; divisor for next step
837 {"<1s", 1},
838 {"%.0fs", 60},
839 {"%.0fm", 60},
840 {"%.0fh", 24},
841 {"%.1fd", 365.25},
842 {"%.1fy", 10},
843 {">10y"},
844}
845local function secondsToTime(timeInUnits)
846 for _,unit in ipairs(timeUnits) do
847 local fmt = unit[1]
848 local divisor = unit[2]
849 if not divisor or timeInUnits < divisor then
850 return fmt:format(timeInUnits)
851 else
852 timeInUnits = timeInUnits / divisor
853 end
854 end
855end
856
857--THE LIBRARY--
858local auto_progress = {}
859--[[
860 auto_progress.new(totalWork) -> {update(),finish(),draw()}
861 creates an object that you have to update with the status of you operation
862 'size' is a measure of the total size of your operation.
863 It can be bytes or items to process, distance to travel or whatever you like.
864 The returned object has the following contents:
865
866 obj:update(workAdded)
867 should be called whenever you have finished a part of your work.
868 'progressAdded' is how much has been done since the last call.
869 (Negative values will remove progress.)
870 It will draw and update the progress bar if necessary.
871
872 obj:finish()
873 should be called when your application finished the operation.
874 It will draw a full progress bar and move to the next line when a progress bar has been drawn previously.
875
876 obj:draw()
877 is normally called by update() and finish().
878 It can be called manually to avoid the delay before the progress bar is drawn.
879
880 obj.x, obj.y
881 can be set to specify where the progress bar is drawn.
882 The default is the beginning of the current line.
883
884 obj.width
885 can be set to limit the width of the progress bar.
886 The default is drawing to the end of the line.
887
888 obj.drawing
889 setting this to true cancels the drawing delay.
890
891 obj.disabled
892 true disables automatic drawing; manual drawing is still possible though.
893
894 obj.totalWork
895 defines the size of the operation (in your own unit)
896 This value is set when creating the object.
897 Change it if you have to update your operation size.
898
899 obj.doneWork
900 defined as how much work has already been done.
901 This value is updated automaticly when using the methods above.
902 But you can still change it if you really want to.
903
904 obj.creationTime
905 the computer.uptime() when the object was created
906
907 obj.lastUpdate
908 the computer.uptime() from the last obj.draw() call
909]]
910function auto_progress.new(self)
911
912 --force self to be a table
913 if type(self) == "number" or self == nil then
914 --create a new table
915 self = {totalWork = self}
916 end
917 assert(type(self) == "table", "Parameter has to be a table, a number or nil!")
918
919 --make default values
920 self.doneWork = self.doneWork or 0
921 self.totalWork = self.totalWork or 1
922 self.disabled = self.disabled or false
923 self.drawing = self.drawing or false
924
925 self.creationTime = computer.uptime()
926 self.lastUpdate = self.creationTime
927
928 --check types
929 assert(self.x == nil or type(self.x) == "number", "progress_state.x has to be nil or a number!")
930 assert(self.y == nil or type(self.y) == "number", "progress_state.y has to be nil or a number!")
931 assert(self.width == nil or type(self.width) == "number", "progress_state.width has to be nil or a number!")
932 assert(type(self.doneWork) == "number", "progress_state.doneWork has to be nil or a number!")
933 assert(type(self.totalWork) == "number", "progress_state.totalWork has to be nil or a number!")
934 assert(type(self.disabled) == "boolean", "progress_state.disabled has to be nil or a boolean!")
935 assert(type(self.drawing) == "boolean", "progress_state.drawing has to be nil or a boolean!")
936
937 --add methods
938 --updates the progress bar, adds workAdded to the progress counter
939 function self.update(workAdded)
940 self.doneWork = self.doneWork + (workAdded or 0)
941 local currentTime = computer.uptime()
942 local triggerTime = self.lastUpdate + (self.drawing and REFRESH_TIME or DRAWING_DELAY)
943 if currentTime > triggerTime and (not self.disabled) then
944 if self.drawing then
945 self.draw()
946 elseif (self.doneWork < self.totalWork * INHIBITING_PROGRESS) then
947 self.draw()
948 end
949 end
950 end
951 --draws a final progress bar if necessary
952 function self.finish()
953 self.doneWork = self.totalWork
954 if self.drawing and (not self.disabled) then
955 self.draw()
956 end
957 end
958 --used for drawing the progress bar
959 function self.draw()
960 self.drawing = true
961 self.lastUpdate = computer.uptime()
962 if term.isAvailable() then
963 --get drawing dimensions
964 local gpu = component.gpu
965 local screen_width, screen_height = gpu.getResolution()
966 if screen_width == nil then
967 --gpu not bound to screen
968 return
969 end
970 --get drawing position: custom position or move to the beginning of the current line
971 if self.x == nil or self.y == nil then
972 local term_x, term_y = term.getCursor()
973 if term_x > 1 and self.x == nil and self.y == nil then
974 --finish line
975 print()
976 end
977 --move cursor to the next line
978 print()
979 --get progress bar position
980 term_x, term_y = term.getCursor()
981 self.x, self.y = self.x or term_x, self.y or (term_y - 1)
982 end
983 local relativeProgress = math.max(math.min(self.doneWork / self.totalWork, 1), 0)
984 local x, y = self.x, self.y
985 if x <= 0 or x > screen_width or y <= 0 or y > screen_height then
986 --don't draw outside the screen
987 return
988 end
989 local width = math.min(self.width or math.huge, (screen_width - x + 1))
990 --calculate progress bar size
991 local barWidth = math.floor((width - 2) * relativeProgress + 0.5)
992 --calculate time to finish
993 local eta = nil
994 if relativeProgress > 0 and relativeProgress < 1 then
995 local speed = relativeProgress / (computer.uptime() - self.creationTime)
996 eta = (1 - relativeProgress) / speed
997 end
998 --create background
999 local line = "[" .. ("-"):rep(barWidth) .. (" "):rep(width - 2 - barWidth) .. "]"
1000 --create text
1001 local text = ("%i%%"):format(100 * relativeProgress)
1002 if eta then
1003 text = text .. " ETA: " .. secondsToTime(eta)
1004 end
1005 --combine text with background
1006 local textPosition = math.floor((width - #text) / 2)
1007 line = line:sub(1, textPosition) .. text .. line:sub(textPosition + #text + 1, -1)
1008 --drawing...
1009 gpu.set(x, y, line)
1010 end
1011 end
1012 return self
1013end
1014
1015return auto_progress
1016usr/man/tar.man0000600000175000017500000000642112477327545010660 0ustar NAME
1017 tar - tar archiver for OpenComputers
1018
1019SYNOPSIS
1020 tar <function letter> [other options] FILES...
1021
1022DESCRIPTION
1023 allows working with tar archives in OpenComputers
1024 It supports the standard tar format + ustar extension.
1025 (not every feature but enough for most uses)
1026
1027 That makes it a nice tool to exchange multiple files and directories between OpenComputers and "real computers".
1028 Tar a bunch of files, use pastebin / wget to transmit the archive and extract it at destination.
1029 Since this program is also able to create tar archives it is possible in both directions!
1030
1031 When you want read from / write to a file you have to use the option -f / --file.
1032 It then uses the first file given as the archive.
1033 Else it uses the primary Computronics tape drive. (with the archive starting at the current position)
1034
1035 A function flag is required to indicate what you want to do with the archive.
1036 Further options can be used to modify the behaviour of the program.
1037
1038 By default the program does not print anything except errors.
1039 Use --verbose to make it 'talk'!
1040
1041 By default it overwrites every existing file.
1042 Take care or use options like --keep-old-files!
1043
1044FUNCTION LETTERS
1045 -c --create
1046 create a new tar archive
1047
1048 -r --append
1049 append to the tar archive
1050 (tape only unless io.open is going to support rb+ mode)
1051
1052 -t --list
1053 list contents of tar archive
1054
1055 -x --extract --get
1056 extract the archive to the current directory
1057
1058 --help
1059 show usage information
1060
1061OTHER OPTIONS
1062 -f --file
1063 tell tar to use the first file parameter as a tar archive
1064 (When creating an archive, it will be the output file.)
1065 Without this option the primary tape drive is used instead.
1066
1067 -h --dereference
1068 follow symlinks (treating them as files / directories)
1069
1070 --exclude=FILE1;FILE2
1071 exclude a semicolon separated list of files
1072 The archive file is automaticly excluded when writing to an archive.
1073 When using the --dereference option it might be necessary to exclude the archive manually.
1074 (i.e. when following a symlink directs the program to include the archive itself)
1075 The program will throw an error if an input file grew while being copied to the archive.
1076 (So it at least terminates when it tries to put the archive into itself.)
1077
1078 -v --verbose
1079 print out what the program is doing
1080 If the library auto_progress is installed it also displays nice progress bars on large files.
1081
1082 --keep-old-files
1083 throw an error when extracting and an output file already exists
1084 --skip-old-files
1085 do not overwrite existing files, but continues without errors
1086 --keep-newer-files
1087 overwrite existing files only if they are older than the one within the archive
1088
1089 --dir=DIR
1090 change the reference directory i.e. to extract a tar file in a subdirectory
1091 (The original tar option is "-C DIR". But shell.parse() does not support an option+parameter combination.)
1092
1093EXAMPLES
1094 tar --create --file all.tar / --exclude=/mnt
1095 creates an archive 'all.tar' containing the whole file system except "/mnt".
1096
1097 tar -xf -v another.tar
1098 extracts the contents of 'another.tar' while showing the user what it's doing.
1099
1100 tar -t
1101 displays the contents of the tape archive.
1102 The current position is read as the beginning of the archive.