CTypes libarchive wrapper, handles extraction only with no arguments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 | #!/usr/bin/env python
################################################################################
# #
# Copyright (c) 2013, Mike 'Fuzzy' Partin <fuzzy@fu-manchu.org> #
# All rights reserved. #
# #
# Redistribution and use in source and binary forms, with or without #
# modification, are permitted provided that the following conditions are met: #
# #
# 1. Redistributions of source code must retain the above copyright notice, #
# this list of conditions and the following disclaimer. #
# 2. Redistributions in binary form must reproduce the above copyright notice, #
# this list of conditions and the following disclaimer in the documentation #
# and/or other materials provided with the distribution. #
# #
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" #
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE #
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE #
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE #
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR #
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS #
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN #
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) #
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE #
# POSSIBILITY OF SUCH DAMAGE. #
# #
# The views and conclusions contained in the software and documentation are #
# those of the authors and should not be interpreted as representing official #
# policies, either expressed or implied, of the FreeBSD Project. #
# #
################################################################################
################################################################################
### Module imports ###
################################################################################
# Stdlib
import os
import sys
import types
import ctypes
################################################################################
### libarchive.so detction and loading ###
################################################################################
_LibPath = None
_LibDirs = [
'/lib',
'/lib32',
'/usr/lib',
'/usr/lib32',
'/usr/pkg/lib',
'/usr/pkg/lib32',
'/usr/local/lib',
'/usr/local/lib32'
]
def _setLibPath(a, d, f):
''' A private method used only as a callback for os.path.walk() '''
global _LibPath
if _LibPath == None:
if 'libarchive.so' in f:
_LibPath = d
## Next find libarchive.so
for path in _LibDirs:
os.path.walk(path, _setLibPath, None)
if _LibPath == None:
raise Exception('ERROR: Could not find libarchive.so on your system.')
## And since we have found it, lets load it
try:
libarchive = ctypes.cdll.LoadLibrary('%s/libarchive.so' % _LibPath)
except OSError as msg:
print(msg)
sys.exit(1)
################################################################################
### libarchive FFI interface ###
################################################################################
class Libarchive:
''' Python CTypes interface to libarchive '''
# These are our structs for libarchive (hopefully)
class Archive(ctypes.Structure):
pass
class ArchiveEntry(ctypes.Structure):
pass
##############################################################################
### Initialization ###
##############################################################################
def __init__(self, fname=None, debug=False):
''' '''
##
## Setup our FFI interface
##
## Reference our global library handle
global libarchive
self.lib = libarchive
## Argument handling/setting
self.__fname = fname
self.__debug = debug
## Constants
self.ARCH_EOF = 1
self.ARCH_OK = 0
self.ARCH_RETRY = -10
self.ARCH_WARN = -20
self.ARCH_FAILED = -25
self.ARCH_FATAL = -30
self.ExtractFlags = 4 | 2 | 32 | 64 # TIME,PERM,ACL,FFLAGS
##
## Now we declare our function symbols and their return types
##
## Beginning with library version detection
self.versionNumber = self.lib.archive_version_number
## Pointer return types
self._entryNew = self.lib.archive_entry_new
self._entryNew.restype = ctypes.POINTER(self.ArchiveEntry)
self._readNew = self.lib.archive_read_new
self._readNew.restype = ctypes.POINTER(self.Archive)
self._writeDiskNew = self.lib.archive_write_disk_new
self._writeDiskNew.restype = ctypes.POINTER(self.Archive)
## Integer return types
# Reading
self._readClose = self.lib.archive_read_close
self._readDataBlock = self.lib.archive_read_data_block
self._readDataSkip = self.lib.archive_read_data_skip
self._readNextHeader = self.lib.archive_read_next_header
self._readOpenFilename = self.lib.archive_read_open_filename
self._readSupportFilterAll = self.lib.archive_read_support_compression_all
self._readSupportFormatAll = self.lib.archive_read_support_format_all
# Writing
self._writeClose = self.lib.archive_write_close
self._writeDataBlock = self.lib.archive_write_data_block
self._writeDiskSetOptions = self.lib.archive_write_disk_set_options
self._writeDiskSetLookup = self.lib.archive_write_disk_set_standard_lookup
self._writeFinishEntry = self.lib.archive_write_finish_entry
self._writeHeader = self.lib.archive_write_header
# Info
self._entrySize = self.lib.archive_entry_size
# This is due to a problem where libarchive version below v3.x do not have
# the archive_read_free symbol. And archive_read_finish will only be kept
# about until version 4 of the library, so no sense in not being forward
# compatible about it.
if int(self.versionNumber()) < int(3001002):
self._readFree = self.lib.archive_read_finish
self._writeFree = self.lib.archive_write_finish
else:
self._readFree = self.lib.archive_read_free
self._writeFree = self.lib.archive_write_free
## String return types
self._entryPathname = self.lib.archive_entry_pathname
self._entryPathname.restype = ctypes.c_char_p
self._errorString = self.lib.archive_error_string
self._errorString.restype = ctypes.c_char_p
##############################################################################
### Private methods ###
##############################################################################
def _copyData(self, archiveR, archiveW):
''' '''
r = ctypes.c_int()
buff = ctypes.c_void_p()
size = ctypes.c_int()
offs = ctypes.c_longlong()
while True:
# Read in a block
r = self._readDataBlock(
archiveR, # Archive (reading)
ctypes.byref(buff), # Buffer pointer
ctypes.byref(size), # Size pointer
ctypes.byref(offs)) # Offset pointer
# Check ourselves
if r == self.ARCH_EOF:
return self.ARCH_OK
if r != self.ARCH_OK:
return r
# Write out a block
r = self._writeDataBlock(
archiveW, # Archive (writing)
buff, # Buffer data
size, # Size data
offs) # Offset data
# And check ourselves again
if r != self.ARCH_OK:
print(self._errorString(archiveB))
return r
def _fmtGauge(self, perc=None):
''' Display a 10 space progress bar '''
buff = '[#'
if perc and type(perc) == types.IntType:
if (perc/10) > 0:
for i in range(0,(perc/10)):
buff += '#'
for i in range(0,(10 - (perc / 10))):
buff += '-'
buff += ']'
# and return our gauge
return buff
else:
return None
def _fmtTime(self, secs=None):
''' '''
if secs and type(secs) in [types.IntType, types.LongType]:
return '%ds' % secs
def _fmtSize(self, size=None):
''' Format a number of bytes into a human readable string '''
if size and type(size) in [types.IntType, types.LongType]:
if size < 1024:
return '%dB' % size
elif size > 1024 and size < (1024**2):
return '%4.02fKB' % (float(size) / 1024.00)
elif size > (1024**2) and size < (1024**3):
return '%4.02fMB' % ((float(size) / 1024.00) / 1024.00)
else:
return '%.02fGB' % (((float(size) / 1024.00) / 1024.00) / 1024.00)
##############################################################################
### Public methods ###
##############################################################################
def listContents(self):
''' List the contents of the archive (returns a list of path/filenames) '''
retv = [] # Return value
archive = self._readNew() # Archive struct
entry = self._entryNew() # Entry struct
# detect compression and archive type
self._readSupportFilterAll(archive)
self._readSupportFormatAll(archive)
# Open, analyse, and close our archive
if self._readOpenFilename(archive, self.__fname, 10240) != self.ARCH_OK:
print(self._errorString(archive))
sys.exit(1)
while self._readNextHeader(archive, ctypes.byref(entry)) == self.ARCH_OK:
retv.append(self._entryPathname(entry))
self._readDataSkip(archive) # Not strictly necessary
if self._readFree(archive) != self.ARCH_OK:
print(self._errorString(archive))
sys.exit(1)
# Return our list of archive entries
return retv
def extractArchive(self):
''' '''
# Get the number of elements in the archive (adds time on large archives)
total = len(self.listContents())
processed = 0
# Setup our structs
arch = self._readNew()
ext = self._writeDiskNew()
entry = self._entryNew()
# detect archive type and compression
self._readSupportFormatAll(arch)
self._readSupportFilterAll(arch)
# set our writer options
self._writeDiskSetOptions(ext, self.ExtractFlags)
self._writeDiskSetLookup(ext)
# open the archive
self._readOpenFilename(arch, self.__fname, 10240)
# get our first header
ret = self._readNextHeader(arch, ctypes.byref(entry))
while ret != self.ARCH_EOF:
if ret != self.ARCH_OK or ret < self.ARCH_WARN:
print(self._errorString(arch))
sys.exit(1)
# write out our header
ret = self._writeHeader(ext, entry)
if ret != self.ARCH_OK:
print(self._errorString(ext))
elif self._entrySize(entry) > 0:
# copy the contents into their new home
self._copyData(arch, ext)
if ret != self.ARCH_OK:
print(self._errorString(ext))
if ret < self.ARCH_WARN:
sys.exit(1)
processed += 1
# And update our progress line
sys.stdout.write('[1;32m>[0m Extracting %-46s %12s (%3d%%)\r' % (
'%s:' % os.path.basename(self.__fname[:57]),
self._fmtGauge(int((float(processed) / float(total)) * 100)),
((float(processed) / float(total)) * 100)
))
sys.stdout.flush()
# close that entry up
ret = self._writeFinishEntry(ext)
if ret != self.ARCH_OK:
print(self._errorString(ext))
if ret < self.ARCH_WARN:
sys.exit(1)
# And get ready to head back to the top
ret = self._readNextHeader(arch, ctypes.byref(entry))
# Cleanup
self._readClose(arch)
self._readFree(arch)
self._writeClose(ext)
self._writeFree(ext)
# And one last update of our progress line
sys.stdout.write('[1;32m>[0m Extracting %-46s %12s (%3d%%)\r' % (
"%s:" % os.path.basename(self.__fname[:57]),
self._fmtGauge(100),
100
))
sys.stdout.flush()
# You did good soldier
return self.ARCH_OK
def createArchive(self, files=None):
''' '''
pass
################################################################################
### Main argument handling / operations ###
################################################################################
if __name__ == '__main__':
try:
# Get rid of the program name argument(0)
sys.argv.pop(0)
# Chug through the remaining arguments and process them
for arg in sys.argv:
# First ensure that the argument is a valid file
if os.path.exists(arg):
obj = Libarchive(fname=arg)
obj.extractArchive()
print('')
# Too bad, we don't exist, piss off
else:
raise Exception('ERROR: %s is not a valid filename.' % arg)
# Lets print our message and get the hell outta here.
except Exception as msg:
print(msg)
sys.exit(1)
|
I moved the libarchive.so detection/loading outside of the Libarchive() class to optimize handling multiple archives on a single commandline.
I added the libarchive version check as it turns out that most distributions of Linux are behind FreeBSD in keeping up with the current versions of libarchive. Debian, Slackware, I'm looking at you two.