Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- """This module provides a full-featured pak archive reader/writer (PAK_Archive) and a simple effecient pak reader called PAK. Basic usage example: [print(f) for f in PAK("data.pak")]"""
- VERSION = 1.03
- import os
- import zlib
- from ctypes import Structure, c_uint32, c_char, c_ubyte, sizeof
- from collections import namedtuple
- from tempfile import NamedTemporaryFile, SpooledTemporaryFile
- PAK_TEMP_FILE_DIRECTORY = os.path.join(os.environ["tmp"], "bz2pak")
- class Header(Structure):
- _fields_ = [
- ("magic", c_char*4),
- ("version", c_uint32),
- ("folder_count", c_uint32),
- ("folder_offset", c_uint32),
- ("file_count", c_uint32),
- ("file_offset", c_uint32),
- ("junk", c_ubyte*32)
- ]
- class FileInfo(Structure):
- _fields_ = [
- ("offset", c_uint32),
- ("comp_size", c_uint32),
- ("real_size", c_uint32)
- ]
- File = namedtuple("File", "folder_index name data_offset comp_size real_size") # Reference to internal pak file
- FileExtern = namedtuple("FileExtern", "folder_index name compression external_path mtime") # Reference to external file
- FileTemp = namedtuple("FileTemp", "folder_index name compression temporary_file") # File stored in memory or on disk when too large
- class PAK:
- def __init__(self, pak_file_path):
- self.pak_file = None # Prevent AttributeError in __del__ if exception is raised
- if not os.path.exists(pak_file_path):
- raise FileNotFoundError("PAK %r does not exist" % pak_file_path)
- if not os.path.isfile(pak_file_path):
- raise IsADirectoryError("PAK %r is not a file" % pak_file_path)
- self.path_is_bytes = isinstance(pak_file_path, bytes)
- self.pak_file = open(pak_file_path, "rb")
- self.header = Header()
- self.pak_file.readinto(self.header)
- if self.header.magic != b"DOCP":
- raise RuntimeError("invalid header identifier %r" % header.magic)
- if self.header.version != 2:
- raise RunTimeError("PAK version %d not supported" % header.version)
- self.file_offset = self.header.file_offset
- self.files = []
- self.folder_offset = self.header.folder_offset
- self.folders = [type(pak_file_path)()] # 0th is root which is the default construction of path input b"" or ""
- def __del__(self):
- if self.pak_file is not None:
- self.pak_file.close()
- def __getitem__(self, index):
- if isinstance(index, slice):
- sl = index
- return [self[i] for i in range(self.header.file_count)[sl.start:sl.stop:sl.step]]
- elif not isinstance(index, int):
- raise TypeError("expected %r for file index by got %r" % (int, type(index)))
- if index < 0:
- index = self.header.file_count + index # negative goes in reverse (e.g. [-1] is final item)
- if index >= self.header.file_count or index < 0:
- raise IndexError("index out of range")
- self.pak_file.seek(self.file_offset)
- while len(self.files) <= index:
- folder_index = c_uint32()
- self.pak_file.readinto(folder_index)
- name_length = c_ubyte()
- self.pak_file.readinto(name_length)
- name = self.pak_file.read(name_length.value)
- if not self.path_is_bytes:
- name = name.decode()
- file_info = FileInfo()
- self.pak_file.readinto(file_info)
- try:
- self.file_offset = self.pak_file.tell()
- self.files += [_PAK_File(self, os.path.join(self._folder(folder_index.value), name), file_info)]
- except IndexError:
- raise IndexError("file %r has out of range folder index %d > %d" % (name, folder_index.value, self.header.folder_count))
- return self.files[index]
- def __iter__(self):
- for index in range(self.header.file_count):
- yield self[index]
- def __len__(self):
- return self.header.file_count
- def _folder(self, index):
- if index >= self.header.folder_count+1: # +1 for root or 'non-folder'
- raise IndexError("index out of range")
- restore_offset = self.pak_file.tell()
- self.pak_file.seek(self.folder_offset, 0)
- while len(self.folders) <= index:
- name_length = c_ubyte()
- self.pak_file.readinto(name_length)
- name = self.pak_file.read(name_length.value)
- if not self.path_is_bytes:
- name = name.decode()
- self.folder_offset = self.pak_file.tell()
- self.folders += [name]
- self.pak_file.seek(restore_offset)
- return self.folders[index]
- class _PAK_File(os.PathLike):
- def __init__(self, pak, virtual_path, file_info):
- # Need to keep this reference of pak so its __del__ doesn't get called until its last file goes out of scope.
- # That's a requirement for using this syntax: with open(PAK("data.pak")[0]) as f: f.read()
- self._pak = pak
- self.pak_file = pak.pak_file
- self.virtual_path = virtual_path
- self.file_info = file_info
- # Temp file will only be created from pak if the user calls open() on this object.
- self.tempfile = None
- def __fspath__(self):
- if self.tempfile is None:
- if not os.path.exists(PAK_TEMP_FILE_DIRECTORY):
- os.makedirs(PAK_TEMP_FILE_DIRECTORY)
- self.tempfile = NamedTemporaryFile(suffix=os.path.basename(self.virtual_path), delete=False, dir=PAK_TEMP_FILE_DIRECTORY)
- self.pak_file.seek(self.file_info.offset)
- is_compressed = (self.file_info.comp_size != self.file_info.real_size)
- if is_compressed:
- self.tempfile.write(zlib.decompress(self.pak_file.read(self.file_info.comp_size)))
- else:
- self.tempfile.write(self.pak_file.read(self.file_info.real_size))
- self.tempfile.close()
- return self.tempfile.name
- def __getitem__(self, item):
- if isinstance(item, slice):
- return self.virtual_path[item.start:item.stop:item.step]
- raise TypeError("unsupported type %r" % type(item))
- def __str__(self):
- return str(self.virtual_path)
- def __repr__(self):
- return "%r" % str(self)
- def __del__(self):
- # Unfortunately python being terminated can leave stranded temp files.
- if self.tempfile is not None:
- if os.path.exists(self.tempfile.name) and os.path.isfile(self.tempfile.name):
- os.unlink(self.tempfile.name)
- # These are used in the older PAK_Archive class
- class PakError(Exception): pass
- class PakFileError(PakError): pass
- class PakFileConflict(PakFileError):
- def __init__(self, msg, index=None):
- self.msg = msg
- self.index = index # Index of conflicting item
- def __str__(self):
- return self.msg
- class PAK_Archive:
- """Full archive class with methods to read, write and modify contents in a multitude of ways."""
- magic = b"DOCP" # 1346588484
- version = 2
- default_compression = 6 # 6 is the default used in pak explorer
- max_path = 255
- encoding = "ascii"
- def __init__(self, pak_file_path=None):
- self.pak_file_file_last_mtime = None
- self.pak_file_path = None
- self.folders = [""] # 0th is root ("") which always exists
- self.files = []
- if pak_file_path:
- self.load(pak_file_path)
- def __str__(self):
- return "<%s %r %d folder paths %d files>" % (
- __class__.__name__,
- self.pak_file_path,
- len(self.folders) - 1,
- len(self.files)
- )
- def _check(self):
- """If the pak file has changed or been deleted this will raise an exception."""
- if self.pak_file_path is None:
- return
- if not os.path.exists(self.pak_file_path):
- # Raises exception if file cannot be opened for reading.
- with open(self.pak_file_path, "rb"): pass
- if os.path.getmtime(self.pak_file_path) != self.pak_file_file_last_mtime:
- raise PakFileError("%r was changed")
- def _validate_file_path(self, path, match_full_path=True):
- """Raises detailed exception if file already exists."""
- if match_full_path:
- if self.file_exists(path, match_full_path=True):
- raise PakFileConflict("file %r already exists" % path, self.get_file_index(path, match_full_path=True))
- else:
- file_name = os.path.basename(path)
- if self.file_exists(file_name, match_full_path=False):
- conflict_index = self.get_file_index(file_name)
- conflict_file = self.files[conflict_index]
- conflict_folder = self.folders[conflict_file.folder_index]
- conflict_path = os.path.join(conflict_folder, conflict_file.name)
- raise PakFileConflict("file %r already exists as %r" % (file_name, conflict_path), conflict_index)
- def get_comparable_path(self, path):
- if not path:
- return ""
- path = os.path.normpath(path)
- path = os.path.normcase(path)
- path = path.replace("/", "\\")
- while path[0] == "\\":
- path = path[1::] # Remove any trailing slashes
- while path[-1] == "\\":
- path = path[0:-1] # Remove any leading slashes
- return path
- def load(self, pak_file_path):
- """Sets the pak file for this object. The file may be overwritten by save()."""
- self.pak_file_file_last_mtime = None
- self.pak_file_file_last_mtime = os.path.getmtime(pak_file_path)
- self.pak_file_path = pak_file_path
- with open(self.pak_file_path, "rb") as f:
- length = c_ubyte()
- header = Header()
- f.readinto(header)
- if header.magic != __class__.magic:
- raise PakError("invalid magic %r" % header.magic)
- if header.version != __class__.version:
- raise PakError("unsupported version %r" % header.version)
- # Paths of folders
- f.seek(header.folder_offset)
- self.folders = [""] # "" is root of pak
- for folder_index in range(header.folder_count):
- f.readinto(length)
- self.folders += [f.read(length.value).decode(encoding=__class__.encoding)]
- # Paths and offets of files
- f.seek(header.file_offset)
- self.files = []
- for file_index in range(header.file_count):
- folder_index = c_uint32()
- info = FileInfo()
- comp_size = c_uint32()
- real_size = c_uint32()
- f.readinto(folder_index)
- f.readinto(length)
- name = f.read(length.value).decode(encoding=__class__.encoding)
- f.readinto(info)
- self.files += [File(folder_index.value, name, info.data_offset, info.comp_size, info.real_size)]
- def save(self, save_to_file=None, keep_backup=True):
- """Create final pak file on disk.
- save_to_file - Location to save pak file to on disk. Defaults to currently opened pak.
- keep_backup - If True, the original pak file opened (if one was loaded) will be backed up.
- """
- self._check()
- if save_to_file is None:
- save_to_file = self.pak_file_path
- # Temp pak is needed because the source pak is read from in the middle of the process of writing the new one.
- tmp_pak = NamedTemporaryFile(suffix=".pak", prefix="bz2pak", delete=False)
- # 1/4 - Header (placeholder)
- tmp_pak.write(b"\0"*sizeof(Header))
- # 2/4 - File Content
- size_info = dict() # Required for temporary/external files we have not yet compressed
- file_content_offset = []
- for file_index, file in enumerate(self.files):
- file_content_offset += [tmp_pak.tell()]
- # Read the compressed file content out of the original pak we are working on
- if type(file) is File:
- with open(self.pak_file_path, "rb") as src:
- src.seek(file.data_offset)
- tmp_pak.write(src.read(file.comp_size))
- elif type(file) in (FileExtern, FileTemp):
- # Read and compress external file into the new temp-pak
- if type(file) is FileExtern:
- with open(file.external_path, "rb") as f:
- data = f.read()
- # Read from tmp file
- else:
- file.temporary_file.seek(0)
- data = file.temporary_file.read()
- real_size = len(data)
- if file.compression > 0:
- data = zlib.compress(data, file.compression)
- comp_size = len(data)
- tmp_pak.write(data)
- size_info[file] = (comp_size, real_size)
- else:
- raise TypeError("file index %d invalid type %r" % (file_index, type(file).__name__))
- # 3/4 - Folder Table
- folder_offset = tmp_pak.tell()
- for folder in self.folders[1::]:
- name_to_bytes = folder.encode(encoding=__class__.encoding)
- tmp_pak.write(c_ubyte(len(name_to_bytes)))
- tmp_pak.write(name_to_bytes)
- # 4/4 - File Table
- file_offset = tmp_pak.tell()
- for file_index, file in enumerate(self.files):
- if type(file) is File:
- name_to_bytes = file.name.encode(encoding=__class__.encoding)[0:__class__.max_path]
- tmp_pak.write(c_uint32(file.folder_index))
- tmp_pak.write(c_ubyte(len(name_to_bytes)))
- tmp_pak.write(name_to_bytes)
- tmp_pak.write(c_uint32(file_content_offset[file_index]))
- tmp_pak.write(c_uint32(file.comp_size))
- tmp_pak.write(c_uint32(file.real_size))
- elif type(file) in (FileExtern, FileTemp):
- if type(file) is FileExtern:
- if not os.path.exists(file.external_path):
- raise FileNotFoundError("external file %r not found for %r index %d" % (file.external_path, file.name, file_index))
- if not os.path.isfile(file.external_path):
- raise IsADirectoryError("external file %r is actually a folder but file was expected for %r index %d" % (file.external_path, file.name, file_index))
- if os.path.getmtime(file.external_path) != file.mtime:
- raise FileExistsError("external file %r was modified since being added for %r index %d" % (file.external_path, file.name, file_index))
- name_to_bytes = file.name.encode(encoding=__class__.encoding)[0:__class__.max_path]
- tmp_pak.write(c_uint32(file.folder_index))
- tmp_pak.write(c_ubyte(len(name_to_bytes)))
- tmp_pak.write(name_to_bytes)
- tmp_pak.write(c_uint32(file_content_offset[file_index]))
- comp_size, real_size = size_info[file]
- tmp_pak.write(c_uint32(comp_size))
- tmp_pak.write(c_uint32(real_size))
- else:
- raise TypeError("invalid type for file %r" % type(file).__name__)
- # Fill in header
- header = Header(__class__.magic, __class__.version, len(self.folders)-1, folder_offset, len(self.files), file_offset, (c_ubyte*32)())
- jump_back = tmp_pak.tell()
- tmp_pak.seek(0)
- tmp_pak.write(header)
- tmp_pak.seek(jump_back)
- if keep_backup:
- backup_dir = os.path.dirname(save_to_file)
- backup_name, backup_ext = os.path.splitext(os.path.basename(save_to_file))
- for i in range(0xFFFFFFFF):
- backup_path = os.path.join(backup_dir, backup_name + "-backup%d" % i + backup_ext)
- if not os.path.exists(backup_path):
- os.rename(save_to_file, backup_path)
- break
- elif os.path.exists(save_to_file):
- os.unlink(save_to_file)
- tmp_pak.close()
- try:
- os.rename(tmp_pak.name, save_to_file)
- except OSError:
- # Just copy and delete old if rename fails
- with open(tmp_pak.name, "rb") as src:
- with open(save_to_file, "wb") as dst:
- dst.write(src.read())
- self.pak_file_path = save_to_file
- self.pak_file_file_last_mtime = os.path.getmtime(self.pak_file_path)
- def read(self, file, match_full_path=False, decompress_data=True):
- """Returns content of file in pak as bytes, decompressing data if necessary.
- file - Index, name, path (if match_full_path is True) or one of the 3 file objects: File, FileTemp, FileExtern.
- match_full_path - file is an absoltue path, otherwise treated as a file name that could match in any file with the name.
- decompress_data - If False, raw compressed data block will be returned, if compressed.
- """
- self._check()
- if type(file) is int:
- file = self.files[file]
- elif type(file) is str:
- file_index = self.get_file_index(file, match_full_path=match_full_path)
- file = self.files[file_index]
- elif type(file) not in (File, FileExtern, FileTemp):
- raise TypeError("invalid type for file %r" % type(file).__name__)
- if type(file) is File:
- with open(self.pak_file_path, "rb") as f:
- is_compressed = file.comp_size != file.real_size
- f.seek(file.data_offset)
- data = f.read(file.comp_size)
- if decompress_data and is_compressed:
- return zlib.decompress(data)
- return data
- elif type(file) is FileExtern:
- with open(file.external_path, "rb") as f:
- return f.read()
- elif type(file) is FileTemp:
- file.temporary_file.seek(0)
- return file.temporary_file.read()
- else:
- raise TypeError("Unsupported type for file %r." % type(file).__name__)
- def get_folder_index(self, folder):
- """Returns index of folder (path or index). Raises exception if not found."""
- if type(folder) is int:
- if folder >= len(self.folders):
- raise IndexError("folder index out of range")
- return folder
- elif type(folder) != str:
- raise TypeError("folder must be str (folder path) or int (folder index), not %r" % type(folder).__name__)
- comparable_folder = self.get_comparable_path(folder)
- for index, compare_folder in enumerate(self.folders):
- if comparable_folder == self.get_comparable_path(compare_folder):
- return index
- raise PakFileError("folder %r not found." % folder)
- def get_file_index(self, file, match_full_path=False, occurrence=0):
- """Returns index of file name or full path if match_full_path is True. Raises exception if not found.
- match_full_path - file is an absoltue path, otherwise treated as a file name that could match in any file with the name.
- occurrence - Which occurrence to match if more than one exist.
- """
- if type(file) is int:
- if file >= len(self.files):
- raise IndexError("file index out of range (%d/%d)" % (file, len(self.files)))
- return file
- elif type(file) != str:
- raise TypeError("file must be str (file path) or int (file index), not %r" % type(file).__name__)
- occurrence_counter = 0
- if match_full_path:
- # Strings with no path will be treated as being in pak root ("")
- file_folder_path = os.path.dirname(file)
- # Get folder index of this path (it must exist)
- file_folder_index = self.get_folder_index(file_folder_path)
- file_name = os.path.basename(file)
- file_name = os.path.normcase(file_name)
- for compare_file_index, compare_file in enumerate(self.files):
- if compare_file.folder_index == file_folder_index:
- # This file is in the same folder. This way we only have to compare ints to check for the folder portion.
- if file_name == os.path.normcase(compare_file.name):
- if occurrence < occurrence_counter:
- occurrence += 1
- else:
- return compare_file_index
- raise PakFileError("file %r not found" % file)
- else:
- file_name = os.path.normcase(file)
- for compare_file_index, compare_file in enumerate(self.files):
- if file_name == os.path.normcase(compare_file.name):
- if occurrence < occurrence_counter:
- occurrence += 1
- else:
- return compare_file_index
- raise PakFileError("no files named %r found" % file)
- def folder_exists(self, folder):
- """Returns True if folder exists, otherwise False."""
- try:
- self.get_folder_index(folder)
- return True
- except IndexError:
- return False
- except PakFileError:
- return False
- def file_exists(self, file, match_full_path=False):
- """Returns True if folder exists, otherwise False."""
- try:
- self.get_file_index(file, match_full_path=match_full_path)
- return True
- except IndexError:
- return False
- except PakFileError:
- return False
- def get_subfolder_indices(self, target_folder=0):
- """Yields index of each subfolder belonging to target_folder (can be index or path)."""
- target_folder_index = self.get_folder_index(target_folder)
- target_folder = self.folders[target_folder_index]
- target_folder = self.get_comparable_path(target_folder)
- for folder_index, folder in enumerate(self.folders[1::]):
- folder_compare = self.get_comparable_path(folder)
- folder_parent = os.path.dirname(folder_compare)
- if folder_parent == target_folder:
- yield folder_index+1
- def add_folder(self, new_folder_path):
- """Create new folder path in pak.
- new_folder_path - Path to create. Must not already exist.
- Returns index of newly created folder path.
- If folder already exists PakFileConflict is raised.
- Call PAK.save() to apply changes.
- """
- self._check()
- new_folder_path = os.path.normpath(new_folder_path)
- if len(new_folder_path) >= __class__.max_path:
- raise PakFileError("folder path %r uses %d/%d characters" % (new_folder_path, len(new_folder_path), __class__.max_path))
- if len(new_folder_path) == 0:
- raise PakFileError("0-length folder path")
- if self.folder_exists(new_folder_path):
- raise PakFileConflict("folder %r already exists" % new_folder_path, self.get_folder_index(new_folder_path))
- self.folders += [new_folder_path]
- return len(self.folders) - 1
- def delete_folder(self, folder, delete_files=False):
- """Delete folder in pak. Note that intermediate-level folders may only implicitly exist.
- folder - Index of or path to folder to delete.
- delete_files - If True, all files will be deleted in all subfolders. Otherwise they are moved to root.
- If a folder is not found matching the index or exact path PakFileError is raised.
- Call PAK.save() to apply changes.
- """
- self._check()
- folder_index = self.get_folder_index(folder)
- if folder_index == 0:
- raise PakFileError("cannot delete root")
- subfolder_indices = list(self.get_subfolder_indices(folder))
- for subfolder in subfolder_indices:
- self.delete_folder(subfolder, delete_files=delete_files)
- file_indices_in_folder = []
- files_indices_affected = []
- for file_index, file in enumerate(self.files):
- if file.folder_index == folder_index:
- file_indices_in_folder += [file_index]
- elif file.folder_index > folder_index:
- # Decrement folder indices later after we're sure no exceptions were thrown
- files_indices_affected += [file]
- for file_index in file_indices_in_folder:
- if delete_files:
- self.delete_file(file_index)
- else:
- # Set each file objects folder index to pak root instead of deleting it
- self.files[file_index] = self.files[file_index]._replace(folder_index=0)
- del self.folders[folder_index]
- # File references to folder indices > folder_index must now be decremented by 1
- for file in files_indices_affected:
- file = file._replace(folder_index=file.folder_index-1)
- def add_file(self, path, external_path_or_data, compression=default_compression, allow_multiple_files=False, overwrite=False):
- """Create new file in pak from data or from external file.
- path - Full path to local pak file to be created. Folders will be created automatically if necessary.
- external_path_or_data - Path (must be str) to external file or bytes-like object for data.
- compression - Valid values 0 (none) to 9 (high).
- allow_multiple_files - If False, raises exception if a file with the name given already exists anywhere in pak.
- overwrite - If True, if the file already exists in pak it will be overwritten, otherwise an exception is raised.
- Returns index of new file.
- Call PAK.save() to apply changes.
- """
- self._check()
- is_external_path = type(external_path_or_data) is str
- if is_external_path:
- external_path_or_data = os.path.abspath(external_path_or_data)
- if not os.path.exists(external_path_or_data):
- raise FileNotFoundError("external file %r not found" % external_path_or_data)
- if not os.path.isfile(external_path_or_data):
- raise IsADirectoryError("external file %r is actually a folder but file was expected" % external_path_or_data)
- folder_index = None
- folder_portion = os.path.dirname(path)
- file_index = None
- file_name = os.path.basename(path)
- if len(file_name) >= __class__.max_path:
- raise PakFileError("file name %r uses %d/%d characters" % (file_name, len(file_name), __class__.max_path))
- if len(file_name) == 0:
- raise PakFileError("0-length file name")
- try:
- self._validate_file_path(path, match_full_path=allow_multiple_files)
- except PakFileConflict as exception:
- if overwrite:
- file_index = exception.index
- folder_index = self.files[file_index].folder_index
- else:
- raise exception
- if folder_index is None:
- try:
- folder_index = self.get_folder_index(folder_portion)
- except PakFileError as exception:
- folder_index = self.add_folder(folder_portion)
- name, ext = os.path.splitext(file_name)
- if is_external_path:
- file = FileExtern(folder_index, file_name, compression, external_path_or_data, os.path.getmtime(external_path_or_data))
- else:
- file = FileTemp(folder_index, file_name, compression, SpooledTemporaryFile(max_size=4096, suffix=ext, prefix="bz2pak"))
- file.temporary_file.write(external_path_or_data)
- if file_index is not None:
- self.files[file_index] = file
- else:
- file_index = len(self.files)
- self.files += [file]
- return file_index
- def delete_file(self, file, match_full_path=False):
- """Removes the file (index or path), raises exception if not found.
- file - Index, name, or full path (if match_full_path is True) of file to delete.
- match_full_path - If True and file is a path, only a file matching the exact path wil be deleted.
- If the file is not found PakFileError is raised.
- Call PAK.save() to apply changes.
- """
- self._check()
- file_index = self.get_file_index(file, match_full_path=match_full_path)
- del self.files[file_index]
- def move_file(self, file, new_path, match_full_path=False, allow_multiple_files=False, overwrite=False):
- """Move existing file to new location.
- file - Index or name of file. If match_full_path is True then file is treated as absolute path instead of name.
- new_path - New full path for pak file location. Folders will be created automatically where necessary.
- match_full_path - If True and file is a path, only a file matching the exact path wil be moved.
- compression - Valid values 0 (none) to 9 (high).
- allow_multiple_files - If False, raises exception if a file with the name given already exists anywhere in pak.
- overwrite - If True, if the file already exists in pak it will be overwritten, otherwise an exception is raised.
- Folders will be created automatically in new_path as needed.
- An exception will be raised if file does not exist, or of new_path does exist and overwritse is False.
- Call PAK.save() to apply changes.
- """
- self._check()
- replace_file_index = None
- file_index = self.get_file_index(file, match_full_path=match_full_path)
- folder_index = None
- new_folder_portion = os.path.dirname(new_path)
- new_file_name = os.path.basename(new_path)
- if len(new_file_name) >= __class__.max_path:
- raise PakFileError("file name %r uses %d/%d characters" % (new_file_name, len(new_file_name), __class__.max_path))
- try:
- self._validate_file_path(new_path, match_full_path=allow_multiple_files)
- except PakFileConflict as exception:
- if overwrite:
- replace_file_index = exception.index
- folder_index = self.files[new_file_index].folder_index
- else:
- raise exception
- if folder_index is None:
- try:
- folder_index = self.get_folder_index(new_folder_portion)
- except PakFileError as exception:
- folder_index = self.add_folder(new_folder_portion)
- if replace_file_index:
- self.files[replace_file_index] = self.files[file_index]
- del self.files[file_index] # Overwrite
- file_index = replace_file_index
- self.files[file_index] = self.files[file_index]._replace(folder_index=folder_index, name=new_file_name)
- def rename_file(self, file, new_name, match_full_path=False, allow_multiple_files=False, overwrite=False):
- """Same as move_file, but a simple name can be given instead of a full path for the new name."""
- if any((c in "\\/") for c in new_name):
- raise PakFileError("name %r contains invalid character %r" % (new_name, invalid))
- file_to_rename_index = self.get_file_index(file, match_full_path=match_full_path)
- file_to_rename = self.files[file_to_rename_index]
- folder = self.folders[file_to_rename.folder_index]
- new_name_to_path = os.path.join(folder, new_name)
- return self.move_file(file_to_rename_index, new_name_to_path, match_full_path=True, allow_multiple_files=allow_multiple_files, overwrite=overwrite)
- def dump(self, output_folder, pak_folder=0, full_paths=True, file_filter=None, overwrite=False):
- """Export the file and folder structure of the pak (decompressing all files).
- output_folder - External folder to write all files and folders to.
- pak_folder - Which pak folder to start exporting from. Can be str path or int (pak folder index).
- full_paths - If True, pak folder paths will be created in output_folder for files to mirror pak hierarchy.
- file_filter - If set, each file will be passed to the callable and only exported if it returns true.
- overwrite - If True, existing files will be overwritten if they exist and files are to be written.
- """
- self._check()
- if not os.path.exists(output_folder):
- os.makedirs(output_folder)
- if type(pak_folder) is int:
- pak_folder = self.folders[pak_folder_index]
- pak_folder_comparable = self.get_comparable_path(pak_folder)
- for file in self.files:
- folder = self.folders[file.folder_index]
- folder_comparable = self.get_comparable_path(folder)
- if pak_folder_comparable != folder_comparable[0:len(pak_folder_comparable)]:
- continue # File is not inside of folder caller wanted to export
- if callable(file_filter) and not file_filter(file):
- continue
- if full_paths:
- file_external_path = os.path.join(output_folder, folder, file.name)
- folder_external_path = os.path.dirname(file_external_path)
- if not os.path.exists(folder_external_path):
- os.makedirs(folder_external_path)
- else:
- file_external_path = os.path.join(output_folder, file.name)
- if os.path.exists(file_external_path):
- if os.path.isdir(file_external_path):
- raise IsADirectoryError("%r is a directory but should be a file" % file_external_path)
- elif not overwrite:
- raise FileExistsError("%r already exists" % file_external_path)
- with open(file_external_path, "wb") as f:
- f.write(self.read(file))
- # Basic usage of PAK class (for pak reading only)
- if __name__ == "__main__":
- # Iterate over all files in pak archive
- for path in PAK(r"D:\Program Files (x86)\Battlezone II 1.2\data.pak"):
- # Show 512 bytes of data from each file
- if path[-4::].casefold() in [".txt"]:
- with open(path, "rb") as f:
- print(path, "\n\t", f.read(512), "\n")
- # You can access files by index
- with open(PAK(r"D:\Program Files (x86)\Battlezone II 1.2\data.pak")[5], "rb") as f: # Get 5th file from pak
- print("Some data:", f.read(64))
- # And you can use slice notation
- for path in PAK(r"D:\Program Files (x86)\Battlezone II 1.2\data.pak")[-5::]: # Get the last 5 files
- print(path)
- # Usage samples of PAK_Archive class
- if 0: #__name__ == "__main__":
- # Loading pak files:
- if os.path.exists("data.pak"):
- pak = PAK_Archive("data.pak")
- # Exporting pak files:
- pak.dump(
- "Export Folder",
- pak_folder="weapons/insane",
- full_paths=True,
- # Only export TXT and ODF files in "weapons/insane" folder (includes subfolders)
- file_filter=lambda file: file.name.casefold()[-4::] in (".odf", ".txt"),
- overwrite=False
- )
- # Create new pak:
- pak = PAK_Archive()
- file_index = pak.add_file("Ancient Aliens/File.txt", b"Bytes-like-data", compression=0, overwrite=False)
- pak.add_file("Code/Python/Old/bz2pak.py", __file__, compression=6, overwrite=False)
- pak.move_file("bz2pak.py", "Code/Python/New/bz2pak moved.py", match_full_path=False, allow_multiple_files=False, overwrite=False)
- pak.rename_file("bz2pak moved.py", "bz2pak renamed.py")
- # 3 ways to delete a pak file:
- # pak.delete_file(file_index) # Index match
- # pak.delete_file("file.txt") # Name match (only deletes first found)
- pak.delete_file("Ancient Aliens/File.txt", match_full_path=True) # Exact path match
- pak.add_folder("Extras")
- christian_folder = pak.add_folder("Extras/Christian")
- f1s_file = pak.add_file("Extras/Christian/Feared_1.txt", b"God Fearing Christian Man is Saved", compression=0)
- pak.rename_file(f1s_file, "Jesus is LORD.txt")
- pak.delete_folder(christian_folder or "Extras/Christian", delete_files=False)
- # Iterating over and reading pak files:
- for file in pak.files:
- print(file)
- data = pak.read(file)
- print("\t", "(%d bytes)" % len(data), data[0:64], "\n")
- # Saving pak files:
- pak.save("bz2pak.pak", keep_backup=False)
- print(pak)
Add Comment
Please, Sign In to add comment