Files
pisi/pisi/archive.py
T
2006-11-27 15:01:13 +00:00

421 lines
15 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
#
# Copyright (C) 2005, TUBITAK/UEKAE
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# Please read the COPYING file.
#
"""Archive module provides access to regular archive file types."""
# standard library modules
import os
import stat
import shutil
import tarfile
import zipfile
import gettext
__trans = gettext.translation('pisi', fallback=True)
_ = __trans.ugettext
# PiSi modules
import pisi
import pisi.util as util
import pisi.context as ctx
class ArchiveError(pisi.Error):
pass
class LZMAError(pisi.Error):
def __init__(self, err):
pisi.Error.__init__(self, _("An error has occured while running LZMA:\n%s") % err)
class ArchiveBase(object):
"""Base class for Archive classes."""
def __init__(self, file_path, atype):
self.file_path = file_path
self.type = atype
def unpack(self, target_dir, clean_dir = False):
self.target_dir = target_dir
# first we check if we need to clean-up our working env.
if os.path.exists(self.target_dir) and clean_dir:
util.clean_dir(self.target_dir)
os.makedirs(self.target_dir)
class ArchiveBinary(ArchiveBase):
"""ArchiveBinary handles binary archive files (usually distrubuted as
.bin files)"""
def __init__(self, file_path, arch_type = "binary"):
super(ArchiveBinary, self).__init__(file_path, arch_type)
def unpack(self, target_dir, clean_dir = False):
super(ArchiveBinary, self).unpack(target_dir, clean_dir)
# we can't unpack .bin files. we'll just move them to target
# directory and leave the dirty job to actions.py ;)
import shutil
target_file = os.path.join(target_dir, os.path.basename(self.file_path))
shutil.copyfile(self.file_path, target_file)
class ArchiveTar(ArchiveBase):
"""ArchiveTar handles tar archives depending on the compression
type. Provides access to tar, tar.gz and tar.bz2 files.
This class provides the unpack magic for tar archives."""
def __init__(self, file_path, arch_type = "tar", no_same_permissions = True, no_same_owner = True):
super(ArchiveTar, self).__init__(file_path, arch_type)
self.tar = None
self.no_same_permissions = no_same_permissions
self.no_same_owner = no_same_owner
def unpack(self, target_dir, clean_dir = False):
"""Unpack tar archive to a given target directory(target_dir)."""
super(ArchiveTar, self).unpack(target_dir, clean_dir)
self.unpack_dir(target_dir)
def unpack_dir(self, target_dir):
rmode = ""
if self.type == 'tar':
rmode = 'r:'
elif self.type == 'targz':
rmode = 'r:gz'
elif self.type == 'tarbz2':
rmode = 'r:bz2'
elif self.type == 'tarlzma':
rmode = 'r:'
self.file_path = self.file_path.rstrip(ctx.const.lzma_suffix)
ret, out, err = util.run_batch("lzma d %s %s" % (self.file_path + ctx.const.lzma_suffix,
self.file_path))
if ret != 0:
raise LZMAError(err)
else:
raise ArchiveError(_("Archive type not recognized"))
self.tar = tarfile.open(self.file_path, rmode)
oldwd = os.getcwd()
os.chdir(target_dir)
uid = os.getuid()
gid = os.getgid()
install_tar_path = util.join_path(ctx.config.tmp_dir(), ctx.const.install_tar)
for tarinfo in self.tar:
# Installing packages (especially shared libraries) is a
# bit tricky. You should also change the inode if you
# change the file, cause the file is opened allready and
# accessed. Removing and creating the file will also
# change the inode and will do the trick (in fact, old
# file will be deleted only when its closed).
#
# Also, tar.extract() doesn't write on symlinks... Not any
# more :).
if self.file_path == install_tar_path:
if os.path.isfile(tarinfo.name) or os.path.islink(tarinfo.name):
try:
os.unlink(tarinfo.name)
except OSError, e:
ctx.ui.warning(e)
self.tar.extract(tarinfo)
# tarfile.extract does not honor umask. It must be honored explicitly.
# see --no-same-permissions option of tar(1), which is the deafult
# behaviour.
#
# Note: This is no good while installing a pisi package. Thats why
# this is optional.
if self.no_same_permissions and not os.path.islink(tarinfo.name):
os.chmod(tarinfo.name, tarinfo.mode & ~ctx.const.umask)
if self.no_same_owner and not os.path.islink(tarinfo.name):
os.chown(tarinfo.name, uid, gid)
os.chdir(oldwd)
self.close()
def add_to_archive(self, file_name, arc_name=None):
"""Add file or directory path to the tar archive"""
if not self.tar:
if self.type == 'tar':
wmode = 'w:'
elif self.type == 'targz':
wmode = 'w:gz'
elif self.type == 'tarbz2':
wmode = 'w:bz2'
elif self.type == 'tarlzma':
wmode = 'w:'
self.file_path = self.file_path.rstrip(ctx.const.lzma_suffix)
else:
raise ArchiveError(_("Archive type not recognized"))
self.tar = tarfile.open(self.file_path, wmode)
self.tar.add(file_name, arc_name)
def close(self):
self.tar.close()
if self.tar.mode == 'wb' and self.type == 'tarlzma':
batch = None
if ctx.config.values.build.compressionlevel:
batch = "lzmash -%s %s" % (ctx.config.values.build.compressionlevel, self.file_path)
else:
batch = "lzmash %s" % self.file_path
ret, out, err = util.run_batch(batch)
if ret != 0:
raise LZMAError(err)
class MyZipFile(zipfile.ZipFile):
def decompressToFile(self, name, outname):
import zlib
import binascii
block_size = 1024 * 1024 * 2
if self.mode not in ("r", "a"):
raise RuntimeError, 'read() requires mode "r" or "a"'
if not self.fp:
raise RuntimeError, \
"Attempt to read ZIP archive that was already closed"
zinfo = self.getinfo(name)
filepos = self.fp.tell()
self.fp.seek(zinfo.file_offset, 0)
destfile = file(outname, 'wb')
if zinfo.compress_type == zipfile.ZIP_STORED:
total_read = 0
crc = None
while total_read < zinfo.compress_size:
if zinfo.compress_size - total_read < block_size:
block_size = zinfo.compress_size - total_read
buff = self.fp.read(block_size)
destfile.write(buff)
total_read += (block_size)
if crc:
crc = binascii.crc32(buff, crc)
else:
crc = binascii.crc32(buff)
destfile.close()
self.fp.seek(filepos, 0)
if crc and crc != zinfo.CRC:
raise zipfile.BadZipfile, "Bad CRC-32 for file %s" % name
return
elif zinfo.compress_type != zipfile.ZIP_DEFLATED:
raise zipfile.BadZipfile, \
"Unsupported compression method %d for file %s" % \
(zinfo.compress_type, name)
if not zlib:
raise RuntimeError, \
"De-compression requires the (missing) zlib module"
# zlib compress/decompress code by Jeremy Hylton of CNRI
dc = zlib.decompressobj(-15)
total_read = 0
crc = None
while total_read < zinfo.compress_size:
if zinfo.compress_size - total_read < block_size:
block_size = zinfo.compress_size - total_read
buff = self.fp.read(block_size)
dcbuff = dc.decompress(dc.unconsumed_tail + buff)
destfile.write(dcbuff)
total_read += block_size
if crc:
crc = binascii.crc32(dcbuff, crc)
else:
crc = binascii.crc32(dcbuff)
# need to feed in unused pad byte so that zlib won't choke
ex = dc.decompress(dc.unconsumed_tail + 'Z') + dc.flush()
if ex:
if crc:
crc = binascii.crc32(ex, crc)
else:
crc = binascii.crc32(ex)
destfile.write(ex)
if crc and crc != zinfo.CRC:
raise zipfile.BadZipfile, "Bad CRC-32 for file %s" % name
destfile.close()
self.fp.seek(filepos, 0)
class ArchiveZip(ArchiveBase):
"""ArchiveZip handles zip archives.
Being a zip archive PiSi packages also use this class
extensively. This class provides unpacking and packing magic for
zip archives."""
symmagic = 2716663808 #long of hex val '0xA1ED0000L'
def __init__(self, file_path, arch_type = "zip", mode = 'r'):
super(ArchiveZip, self).__init__(file_path, arch_type)
self.zip_obj = MyZipFile(self.file_path, mode)
def close(self):
"""Close the zip archive."""
self.zip_obj.close()
def add_to_archive(self, file_name, arc_name=None):
"""Add file or directory path to the zip file"""
# It's a pity that zipfile can't handle unicode strings. Grrr!
file_name = str(file_name)
if os.path.isdir(file_name) and not os.path.islink(file_name):
self.zip_obj.writestr(file_name + '/', '')
attr_obj = self.zip_obj.getinfo(file_name + '/')
attr_obj.external_attr = stat.S_IMODE(os.stat(file_name)[0]) << 16L
for f in os.listdir(file_name):
self.add_to_archive(os.path.join(file_name, f))
else:
if os.path.islink(file_name):
dest = os.readlink(file_name)
attr = zipfile.ZipInfo()
attr.filename = file_name
attr.create_system = 3
attr.external_attr = self.symmagic
self.zip_obj.writestr(attr, dest)
else:
comp_type = zipfile.ZIP_DEFLATED
if file_name.endswith(".lzma"):
comp_type = zipfile.ZIP_STORED
self.zip_obj.write(file_name, arc_name, comp_type)
if not arc_name:
zinfo = self.zip_obj.getinfo(file_name)
else:
zinfo = self.zip_obj.getinfo(arc_name)
zinfo.create_system = 3
def add_basename_to_archive(self, file_name):
"""Add only the basepath to the zip file. For example; if the given
file_name parameter is /usr/local/bin/somedir, this function
will create only the base directory/file somedir in the
archive."""
cwd = os.getcwd()
path_name = os.path.dirname(file_name)
file_name = os.path.basename(file_name)
if path_name:
os.chdir(path_name)
self.add_to_archive(file_name)
os.chdir(cwd)
def has_file(self, file_path):
""" Returns true if file_path is member of the zip archive"""
return file_path in self.zip_obj.namelist()
def read_file(self, file_path):
return self.zip_obj.read(file_path)
def unpack_file_cond(self, pred, target_dir, archive_root = ''):
"""Unpack/Extract files according to predicate function
pred: filename -> bool
unpacks stuff into target_dir and only extracts files
from archive_root, treating it as the archive root"""
zip_obj = self.zip_obj
for info in zip_obj.infolist():
if pred(info.filename): # check if condition holds
# below code removes that, so we find it here
is_dir = info.filename.endswith('/')
# calculate output file name
if archive_root == '':
outpath = info.filename
else:
# change archive_root
if util.subpath(archive_root, info.filename):
outpath = util.removepathprefix(archive_root,
info.filename)
else:
continue # don't extract if not under
ofile = os.path.join(target_dir, outpath)
if is_dir: # this is a directory
d = os.path.join(target_dir, outpath)
if not os.path.isdir(d):
os.makedirs(d)
perm = info.external_attr
perm &= 0xFFFF0000
perm >>= 16
perm |= 0x00000100
os.chmod(d, perm)
continue
# check that output dir is present
util.check_dir(os.path.dirname(ofile))
# remove output file we might be overwriting.
# (also check for islink? for broken symlinks...)
if os.path.isfile(ofile) or os.path.islink(ofile):
os.remove(ofile)
if info.external_attr == self.symmagic:
if os.path.isdir(ofile):
shutil.rmtree(ofile) # a rare case, the file used to be a dir, now it is a symlink!
target = zip_obj.read(info.filename)
os.symlink(target, ofile)
else:
perm = info.external_attr
perm &= 0x08FF0000
perm >>= 16
perm |= 0x00000100
zip_obj.decompressToFile(info.filename, ofile)
os.chmod(ofile, perm)
def unpack_files(self, paths, target_dir):
self.unpack_file_cond(lambda f:f in paths, target_dir)
def unpack_dir(self, path, target_dir):
self.unpack_file_cond(lambda f:util.subpath(path, f), target_dir)
def unpack_dir_flat(self, path, target_dir):
self.unpack_file_cond(lambda f:util.subpath(path, f), target_dir, path)
def unpack(self, target_dir, clean_dir=False):
super(ArchiveZip, self).unpack(target_dir, clean_dir)
self.unpack_file_cond(lambda f: True, target_dir)
self.close()
return
class Archive:
"""Archive is the main factory for ArchiveClasses, regarding the
Abstract Factory Pattern :)."""
def __init__(self, file_path, arch_type):
"""accepted archive types:
targz, tarbz2, zip, tar"""
handlers = {
'targz': ArchiveTar,
'tarbz2': ArchiveTar,
'tarlzma': ArchiveTar,
'tar': ArchiveTar,
'zip': ArchiveZip,
'binary': ArchiveBinary
}
self.archive = handlers.get(arch_type)(file_path, arch_type)
def unpack(self, target_dir, clean_dir = False):
self.archive.unpack(target_dir, clean_dir)
def unpack_files(self, files, target_dir):
self.archive.unpack_files(files, target_dir)