support for ShrinkIt archives in addition to disk images

This commit is contained in:
Ivan X 2015-12-31 01:25:41 +09:00
parent 01d3ce1775
commit 53404e7784

View File

@ -1,12 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python2.7
# vim: set tabstop=4 shiftwidth=4 expandtab filetype=python: # vim: set tabstop=4 shiftwidth=4 expandtab filetype=python:
"""cppo: Copy or catalog one or all files from a ProDOS raw disk image. """cppo: Copy or catalog one or all files from a ProDOS raw disk image.
copy all files: copy all files:
cppo [-uc] [-ad|-e] imagefile target_directory cppo [-uc] [-shk] [-ad|-e] imagefile target_directory
copy one file: copy one file:
cppo [-uc] [-ad|-e] imagefile /FULL/PRODOS/FILE/PATH target_path cppo [-uc] [-ad|-e] imagefile /FULL/PRODOS/PATH target_path
catalog image: catalog image:
cppo [-uc] -cat imagefile cppo [-uc] -cat imagefile
@ -15,6 +15,7 @@ catalog image:
forks, for adding to ShrinkIt archives with Nulib2 forks, for adding to ShrinkIt archives with Nulib2
using its -e option. using its -e option.
-uc : Copy GS/OS mixed case filenames as uppercase. -uc : Copy GS/OS mixed case filenames as uppercase.
-shk: Use ShrinkIt archive instead of disk image; must be used with -ad.
Wildcard matching/globbing (*) is not supported. Wildcard matching/globbing (*) is not supported.
No verification or validation of the disk image is performed. No verification or validation of the disk image is performed.
@ -39,6 +40,9 @@ import sys
import os import os
import time import time
import datetime import datetime
import shutil
import errno
import uuid
# Intentially fails on pre-2.6 so user can see what's wrong # Intentially fails on pre-2.6 so user can see what's wrong
b'ERROR: cppo requires Python 2.6 or later, including 3.x.' b'ERROR: cppo requires Python 2.6 or later, including 3.x.'
@ -73,6 +77,7 @@ g.AD = 0
g.EX = 0 g.EX = 0
g.DIR = 0 g.DIR = 0
g.UC = 0 g.UC = 0
g.SHK = 0
g.silent = 0 g.silent = 0
# functions # functions
@ -137,8 +142,8 @@ def getFileName(arg1, arg2):
for i in range(0, len(fileName)): for i in range(0, len(fileName)):
if (caseMask[i] == "1"): if (caseMask[i] == "1"):
fileName = (fileName[:i] + fileName = (fileName[:i] +
fileName[i].lower() + fileName[i:i+1].lower() +
fileName[(i+1):]) fileName[i+1:])
return fileName return fileName
def getCaseMask(arg1, arg2): def getCaseMask(arg1, arg2):
@ -223,8 +228,8 @@ def getWorkingDirName(arg1, arg2=None):
for i in range(0, len(workingDirName)): for i in range(0, len(workingDirName)):
if (caseMask[i] == "1"): if (caseMask[i] == "1"):
workingDirName = (workingDirName[:i] + workingDirName = (workingDirName[:i] +
workingDirName[i].lower() + workingDirName[i:i+1].lower() +
workingDirName[(i+1):]) workingDirName[i+1:])
return workingDirName return workingDirName
def getDirEntryCount(arg1): def getDirEntryCount(arg1):
@ -237,6 +242,17 @@ def getDirNextChunkPointer(arg1):
return (readcharDec(g.imageData, start+2) + return (readcharDec(g.imageData, start+2) +
readcharDec(g.imageData, start+3)*256) readcharDec(g.imageData, start+3)*256)
def toProDOSName(name):
i=0
if (name[0:1] == '.'): # eliminate leading period
name = name[1:]
for c in name:
if (c != '.' and not c.isalnum()):
name = name[:i] + '.' + name[i+1:]
i+=1
name = name[0:15]
return name
# -- script begins in earnest here # -- script begins in earnest here
def copyFile(arg1, arg2): def copyFile(arg1, arg2):
@ -324,19 +340,29 @@ def processDir(arg1, arg2=None, arg3=None, arg4=None, arg5=None):
break break
def processEntry(arg1, arg2): def processEntry(arg1, arg2):
# arg1=block number, or subdir name if g.SHK=1
# arg2=index number of entry in directory, or file name if g.SHK=1
# ShrinkIt mode (g.SHK=1) requires -e extended filenames
''' '''
print(getFileName(arg1, arg2), getStorageType(arg1, arg2), print(getFileName(arg1, arg2), getStorageType(arg1, arg2),
getFileType(arg1, arg2), getKeyPointer(arg1, arg2), getFileType(arg1, arg2), getKeyPointer(arg1, arg2),
getFileLength(arg1, arg2), getAuxType(arg1, arg2), getFileLength(arg1, arg2), getAuxType(arg1, arg2),
getCreationDate(arg1, arg2), getModifiedDate(arg1, arg2)) getCreationDate(arg1, arg2), getModifiedDate(arg1, arg2))
''' '''
g.activeFileName = getFileName(arg1 ,arg2).decode("L1") shk_rfork = (1 if (g.SHK and (arg2[-1:] == "r")) else 0)
g.activeFileSize = getFileLength(arg1, arg2) if not g.SHK:
g.activeFileName = getFileName(arg1 ,arg2).decode("L1")
g.activeFileSize = getFileLength(arg1, arg2)
else:
filePath = (os.path.join(arg1, arg2))
g.activeFileName = arg2.split('#')[0]
if ((not g.PDOSPATH_INDEX) or if (not g.PDOSPATH_INDEX or
(g.activeFileName.upper() == g.PDOSPATH_SEGMENT.upper())): g.activeFileName.upper() == g.PDOSPATH_SEGMENT.upper()):
if (getStorageType(arg1, arg2) == 13): # if ProDOS directory if (not g.SHK and
getStorageType(arg1, arg2) == 13): # if ProDOS directory
if not g.PDOSPATH_INDEX: if not g.PDOSPATH_INDEX:
g.targetDir = (g.targetDir + "/" + g.activeFileName) g.targetDir = (g.targetDir + "/" + g.activeFileName)
g.ADdir = (g.targetDir + "/.AppleDouble") g.ADdir = (g.targetDir + "/.AppleDouble")
@ -352,9 +378,10 @@ def processEntry(arg1, arg2):
if not g.PDOSPATH_INDEX: if not g.PDOSPATH_INDEX:
g.targetDir = g.targetDir.rsplit("/", 1)[0] g.targetDir = g.targetDir.rsplit("/", 1)[0]
g.ADdir = (g.targetDir + "/.AppleDouble") g.ADdir = (g.targetDir + "/.AppleDouble")
else: # if ProDOS file else: # if ProDOS file either from image or ShrinkIt archive
if not g.PDOSPATH_INDEX: if not g.PDOSPATH_INDEX:
print(" " + g.activeFileName) print(" " + g.activeFileName +
(" [resource fork]" if shk_rfork else ""))
if g.DIR: if g.DIR:
return return
if not g.targetName: if not g.targetName:
@ -364,19 +391,32 @@ def processEntry(arg1, arg2):
getFileType(arg1, arg2).lower() + getFileType(arg1, arg2).lower() +
getAuxType(arg1, arg2).lower()) getAuxType(arg1, arg2).lower())
touch(g.targetDir + "/" + g.targetName) touch(g.targetDir + "/" + g.targetName)
if g.AD: makeADfile() if g.AD:
copyFile(arg1, arg2) makeADfile()
saveFile((g.targetDir + "/" + g.targetName), g.outFileData) if not g.SHK: # ProDOS image
creationDate = getCreationDate(arg1, arg2) copyFile(arg1, arg2)
modifiedDate = getModifiedDate(arg1, arg2) saveFile((g.targetDir + "/" + g.targetName), g.outFileData)
if (creationDate is None and modifiedDate is not None): creationDate = getCreationDate(arg1, arg2)
modifiedDate = getModifiedDate(arg1, arg2)
if (creationDate is None and modifiedDate is not None):
creationDate = modifiedDate
elif (creationDate is not None and modifiedDate is None):
modifiedDate = creationDate
elif (creationDate is None and modifiedDate is None):
creationDate = (datetime.datetime.today() -
datetime.datetime(1970,1,1)).days*24*60*60
modifiedDate = creationDate
else: # ShrinkIt archive
modifiedDate = int(time.mktime(
time.strptime(
time.ctime(
os.path.getmtime(filePath)))))
creationDate = modifiedDate creationDate = modifiedDate
elif (creationDate is not None and modifiedDate is None): if not shk_rfork:
modifiedDate = creationDate shutil.move(filePath, (g.targetDir + "/" + g.targetName))
elif (creationDate is None and modifiedDate is None): else: # shk_rfork:
creationDate = (datetime.datetime.today() - with open(filePath, 'rb') as infile:
datetime.datetime(1970,1,1)).days*24*60*60 g.adFileData += infile.read()
modifiedDate = creationDate
if g.AD: # AppleDouble if g.AD: # AppleDouble
# set dates # set dates
ADfilePath = (g.ADdir + "/" + g.targetName) ADfilePath = (g.ADdir + "/" + g.targetName)
@ -390,8 +430,10 @@ def processEntry(arg1, arg2):
writechars(g.adFileData, 653, b'p') writechars(g.adFileData, 653, b'p')
writecharsHex(g.adFileData, writecharsHex(g.adFileData,
654, 654,
(getFileType(arg1, arg2) + ((getFileType(arg1, arg2) +
getAuxType(arg1, arg2))) getAuxType(arg1, arg2)) if not g.SHK else
(arg2.split('#')[1][0:2] +
arg2.split('#')[1][2:6])))
writechars(g.adFileData, 657, b'pdos') writechars(g.adFileData, 657, b'pdos')
saveFile(ADfilePath, g.adFileData) saveFile(ADfilePath, g.adFileData)
touch((g.targetDir + "/" + g.targetName), modifiedDate) touch((g.targetDir + "/" + g.targetName), modifiedDate)
@ -511,9 +553,9 @@ def syncExit():
# saveFile(g.imageFile, g.imageData) # saveFile(g.imageFile, g.imageData)
sys.exit(0) sys.exit(0)
def usage(): def usage(exitcode=1):
print(sys.modules[__name__].__doc__) print(sys.modules[__name__].__doc__)
sys.exit(1) sys.exit(exitcode)
# --- ID bashbyter functions (adapted) # --- ID bashbyter functions (adapted)
@ -678,7 +720,10 @@ def writecharsHex(arg1, arg2, arg3):
def slyce(val, start_pos=0, length=1, reverse=False): def slyce(val, start_pos=0, length=1, reverse=False):
"""returns slice of object (but not a slice object) """returns slice of object (but not a slice object)
allows specifying length, and 3.x "bytes" consistency""" allows specifying length, and 3.x "bytes" consistency"""
the_slyce = val[start_pos:start_pos+length] if (start_pos < 0):
the_slyce = val[start_pos:]
else:
the_slyce = val[start_pos:start_pos+length]
return (the_slyce[::-1] if reverse else the_slyce) return (the_slyce[::-1] if reverse else the_slyce)
def to_hex(val): def to_hex(val):
@ -780,6 +825,7 @@ def get_object_names(cls, include_subclasses=True):
def touch(filePath, modTime=None): def touch(filePath, modTime=None):
# http://stackoverflow.com/questions/1158076/implement-touch-using-python # http://stackoverflow.com/questions/1158076/implement-touch-using-python
# print(filePath)
import os import os
if (os.name == "nt"): if (os.name == "nt"):
if filePath[-1] == ".": filePath += "-" if filePath[-1] == ".": filePath += "-"
@ -804,8 +850,9 @@ def makedirs(dirPath):
dirPath = dirPath.replace("./", ".-/") dirPath = dirPath.replace("./", ".-/")
try: try:
os.makedirs(dirPath) os.makedirs(dirPath)
except FileExistsError: except OSError as e:
pass if (e.errno != errno.EEXIST):
raise
def loadFile(filePath): def loadFile(filePath):
import os import os
@ -844,26 +891,36 @@ args = sys.argv
if (len(args) == 1): if (len(args) == 1):
usage() usage()
if (args[1] == "-s"): while (slyce(args[1],0,1) == "-"):
g.silent = 1 if (args[1] == "-s"):
args = args[1:] #shift g.silent = 1
args = args[1:] #shift
if (args[1] == "-uc"): elif (args[1] == "-uc"):
g.UC = 1 g.UC = 1
args = args[1:] #shift args = args[1:] #shift
if (args[1] == "-ad"): elif (args[1] == "-ad"):
g.AD = 1 if g.EX: usage()
args = args[1:] #shift g.AD = 1
args = args[1:] #shift
if (args[1] == "-e"): elif (args[1] == "-shk"):
if g.AD: usage() g.SHK = 1
g.EX = 1 args = args[1:] #shift
args = args[1:] #shift
if (args[1] == "-cat"): elif (args[1] == "-e"):
g.DIR = 1 if g.AD: usage()
args = args[1:] g.EX = 1
args = args[1:] #shift
elif (args[1] == "-cat"):
if g.AD or g.EX: usage()
g.DIR = 1
args = args[1:] #shift
else:
usage()
if not ((g.DIR and len(args) >= 2) or (len(args) >= 3)): if not ((g.DIR and len(args) >= 2) or (len(args) >= 3)):
usage() usage()
@ -876,6 +933,62 @@ g.imageFile = args[1]
if not os.path.isfile(g.imageFile): if not os.path.isfile(g.imageFile):
print("Source " + g.imageFile + " was not found.") print("Source " + g.imageFile + " was not found.")
sys.exit(2) sys.exit(2)
# automatically set ShrinkIt mode if extension suggests it
if (g.SHK or
g.imageFile[-3:].lower() == "shk" or
g.imageFile[-3:].lower() == "sdk" or
g.imageFile[-3:].lower() == "bxy"):
if not g.AD:
print("ShrinkIt archives must be used with -ad option.")
sys.exit(2)
elif (g.DIR or g.EX):
usage()
elif (len(args) == 4):
print("Only entire ShrinkIt archives can be extracted, not one file.")
usage(2)
elif (not os.path.isfile("/usr/local/bin/nulib2") and
not os.path.isfile("/usr/bin/nulib2") and
not os.path.isfile("nulib2")):
print("Nulib2 not found. Can't expand ShrinkIt archive.")
sys.exit(2)
else:
g.SHK = 1
if g.SHK:
unshkdir = ("/tmp/cppo-" + str(uuid.uuid4()))
makedirs(unshkdir)
os.system("cd " + unshkdir + "; " +
"nulib2 -xse " + os.path.abspath(g.imageFile) + " > /dev/null")
fileNames = [name for name in os.listdir(unshkdir)
if not name.startswith(".")]
if (len(fileNames) == 1 and os.path.isdir(unshkdir + "/" + fileNames[0])):
oneDir = True
volumeName=toProDOSName(fileNames[0])
else:
oneDir = False
volumeName = toProDOSName(os.path.basename(g.imageFile))
imageFileExt = os.path.splitext(g.imageFile)[1].upper()
if (volumeName[-4:].lower() == ".shk" or
volumeName[-4:].lower() == ".sdk" or
volumeName[-4:].lower() == ".bxy"):
volumeName = volumeName[0:-4]
# recursively process unshrunk archive hierarchy
for dirName, subdirList, fileList in os.walk(unshkdir):
subdirList.sort()
g.targetDir = (args[2] + (("/" + volumeName) if not oneDir else "") +
("/" if (dirName.count('/') > 2) else "") +
"/".join(dirName.split('/')[3:])) # chop off tempdir
g.ADdir = (g.targetDir + "/.AppleDouble")
print("/".join(dirName.split('/')[3:]))
makedirs(g.targetDir)
makedirs(g.ADdir)
for fname in sorted(fileList):
processEntry(dirName, fname)
if (unshkdir.count('/') > 2):
unshkdir = "/".join(dirName.split('/')[0:3])
shutil.rmtree(unshkdir, True)
syncExit()
g.imageData = loadFile(g.imageFile) g.imageData = loadFile(g.imageFile)
if (len(args) == 4): if (len(args) == 4):