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:
"""cppo: Copy or catalog one or all files from a ProDOS raw disk image.
copy all files:
cppo [-uc] [-ad|-e] imagefile target_directory
cppo [-uc] [-shk] [-ad|-e] imagefile target_directory
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:
cppo [-uc] -cat imagefile
@ -15,6 +15,7 @@ catalog image:
forks, for adding to ShrinkIt archives with Nulib2
using its -e option.
-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.
No verification or validation of the disk image is performed.
@ -39,6 +40,9 @@ import sys
import os
import time
import datetime
import shutil
import errno
import uuid
# 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.'
@ -73,6 +77,7 @@ g.AD = 0
g.EX = 0
g.DIR = 0
g.UC = 0
g.SHK = 0
g.silent = 0
# functions
@ -137,8 +142,8 @@ def getFileName(arg1, arg2):
for i in range(0, len(fileName)):
if (caseMask[i] == "1"):
fileName = (fileName[:i] +
fileName[i].lower() +
fileName[(i+1):])
fileName[i:i+1].lower() +
fileName[i+1:])
return fileName
def getCaseMask(arg1, arg2):
@ -223,8 +228,8 @@ def getWorkingDirName(arg1, arg2=None):
for i in range(0, len(workingDirName)):
if (caseMask[i] == "1"):
workingDirName = (workingDirName[:i] +
workingDirName[i].lower() +
workingDirName[(i+1):])
workingDirName[i:i+1].lower() +
workingDirName[i+1:])
return workingDirName
def getDirEntryCount(arg1):
@ -237,6 +242,17 @@ def getDirNextChunkPointer(arg1):
return (readcharDec(g.imageData, start+2) +
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
def copyFile(arg1, arg2):
@ -324,19 +340,29 @@ def processDir(arg1, arg2=None, arg3=None, arg4=None, arg5=None):
break
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),
getFileType(arg1, arg2), getKeyPointer(arg1, arg2),
getFileLength(arg1, arg2), getAuxType(arg1, arg2),
getCreationDate(arg1, arg2), getModifiedDate(arg1, arg2))
'''
g.activeFileName = getFileName(arg1 ,arg2).decode("L1")
g.activeFileSize = getFileLength(arg1, arg2)
shk_rfork = (1 if (g.SHK and (arg2[-1:] == "r")) else 0)
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
(g.activeFileName.upper() == g.PDOSPATH_SEGMENT.upper())):
if (getStorageType(arg1, arg2) == 13): # if ProDOS directory
if (not g.PDOSPATH_INDEX or
g.activeFileName.upper() == g.PDOSPATH_SEGMENT.upper()):
if (not g.SHK and
getStorageType(arg1, arg2) == 13): # if ProDOS directory
if not g.PDOSPATH_INDEX:
g.targetDir = (g.targetDir + "/" + g.activeFileName)
g.ADdir = (g.targetDir + "/.AppleDouble")
@ -352,9 +378,10 @@ def processEntry(arg1, arg2):
if not g.PDOSPATH_INDEX:
g.targetDir = g.targetDir.rsplit("/", 1)[0]
g.ADdir = (g.targetDir + "/.AppleDouble")
else: # if ProDOS file
else: # if ProDOS file either from image or ShrinkIt archive
if not g.PDOSPATH_INDEX:
print(" " + g.activeFileName)
print(" " + g.activeFileName +
(" [resource fork]" if shk_rfork else ""))
if g.DIR:
return
if not g.targetName:
@ -364,19 +391,32 @@ def processEntry(arg1, arg2):
getFileType(arg1, arg2).lower() +
getAuxType(arg1, arg2).lower())
touch(g.targetDir + "/" + g.targetName)
if g.AD: makeADfile()
copyFile(arg1, arg2)
saveFile((g.targetDir + "/" + g.targetName), g.outFileData)
creationDate = getCreationDate(arg1, arg2)
modifiedDate = getModifiedDate(arg1, arg2)
if (creationDate is None and modifiedDate is not None):
if g.AD:
makeADfile()
if not g.SHK: # ProDOS image
copyFile(arg1, arg2)
saveFile((g.targetDir + "/" + g.targetName), g.outFileData)
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
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
if not shk_rfork:
shutil.move(filePath, (g.targetDir + "/" + g.targetName))
else: # shk_rfork:
with open(filePath, 'rb') as infile:
g.adFileData += infile.read()
if g.AD: # AppleDouble
# set dates
ADfilePath = (g.ADdir + "/" + g.targetName)
@ -390,8 +430,10 @@ def processEntry(arg1, arg2):
writechars(g.adFileData, 653, b'p')
writecharsHex(g.adFileData,
654,
(getFileType(arg1, arg2) +
getAuxType(arg1, arg2)))
((getFileType(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')
saveFile(ADfilePath, g.adFileData)
touch((g.targetDir + "/" + g.targetName), modifiedDate)
@ -511,9 +553,9 @@ def syncExit():
# saveFile(g.imageFile, g.imageData)
sys.exit(0)
def usage():
def usage(exitcode=1):
print(sys.modules[__name__].__doc__)
sys.exit(1)
sys.exit(exitcode)
# --- ID bashbyter functions (adapted)
@ -678,7 +720,10 @@ def writecharsHex(arg1, arg2, arg3):
def slyce(val, start_pos=0, length=1, reverse=False):
"""returns slice of object (but not a slice object)
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)
def to_hex(val):
@ -780,6 +825,7 @@ def get_object_names(cls, include_subclasses=True):
def touch(filePath, modTime=None):
# http://stackoverflow.com/questions/1158076/implement-touch-using-python
# print(filePath)
import os
if (os.name == "nt"):
if filePath[-1] == ".": filePath += "-"
@ -804,8 +850,9 @@ def makedirs(dirPath):
dirPath = dirPath.replace("./", ".-/")
try:
os.makedirs(dirPath)
except FileExistsError:
pass
except OSError as e:
if (e.errno != errno.EEXIST):
raise
def loadFile(filePath):
import os
@ -844,26 +891,36 @@ args = sys.argv
if (len(args) == 1):
usage()
if (args[1] == "-s"):
g.silent = 1
args = args[1:] #shift
while (slyce(args[1],0,1) == "-"):
if (args[1] == "-s"):
g.silent = 1
args = args[1:] #shift
elif (args[1] == "-uc"):
g.UC = 1
args = args[1:] #shift
if (args[1] == "-uc"):
g.UC = 1
args = args[1:] #shift
elif (args[1] == "-ad"):
if g.EX: usage()
g.AD = 1
args = args[1:] #shift
elif (args[1] == "-shk"):
g.SHK = 1
args = args[1:] #shift
if (args[1] == "-ad"):
g.AD = 1
args = args[1:] #shift
elif (args[1] == "-e"):
if g.AD: usage()
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
if (args[1] == "-e"):
if g.AD: usage()
g.EX = 1
args = args[1:] #shift
if (args[1] == "-cat"):
g.DIR = 1
args = args[1:]
else:
usage()
if not ((g.DIR and len(args) >= 2) or (len(args) >= 3)):
usage()
@ -876,6 +933,62 @@ g.imageFile = args[1]
if not os.path.isfile(g.imageFile):
print("Source " + g.imageFile + " was not found.")
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)
if (len(args) == 4):