OXYGENHUB
◆ main openmsx / build / extract.py
3.5 KiB · Python · last changed chore(): initial commit 1 month ago source blame history raw
1
# Extract files from archives.
2
3
from pathlib import Path
4
from os import O_CREAT, O_WRONLY, fdopen, open as osopen, utime
5
try:
6
	from os import O_BINARY
7
except ImportError:
8
	# Platforms that do not define O_BINARY do not need it either.
9
	O_BINARY = 0
10
try:
11
	from os import symlink
12
except ImportError:
13
	def symlink(source, link_name):
14
		raise OSError('OS does not support symlink creation')
15
from os.path import abspath, isdir, join as joinpath, sep, split as splitpath
16
from stat import S_IRWXU, S_IRWXG, S_IRWXO, S_IXUSR, S_IXGRP, S_IXOTH
17
from tarfile import TarFile
18
import sys
19
20
from detectsys import detectOS
21
22
hostOS = detectOS()
23
24
# Note: Larger buffers might make extraction slower.
25
bufSize = 16384
26
27
def extract(archivePath, destDir, rename = None):
28
	'''Extract the given archive to the given directory.
29
	If a rename function is given, it is called with the output path relative
30
	to the destination directory; the value returned by the rename function is
31
	used as the actual relative destination file path.
32
	This function sets file ownership and permissions like is done in newly
33
	created files and ignores the ownership and permissions from the archive,
34
	since we are not restoring a backup.
35
	'''
36
	absDestDir = Path(destDir).resolve()
37
	if not absDestDir.is_dir():
38
		raise ValueError(
39
			f'Destination directory "{absDestDir}" does not exist'
40
		)
41
42
	createdDirs = set()
43
	with TarFile.open(archivePath, errorlevel=2) as tar:
44
		for member in tar.getmembers():
45
			absMemberPath = absDestDir / member.name
46
			if not absMemberPath.is_relative_to(absDestDir):
47
				raise ValueError(
48
					f'Refusing to extract tar entry "{member.name}" '
49
					'outside destination directory'
50
				)
51
			if rename:
52
				absMemberPath = absDestDir / rename(
53
					absMemberPath.relative_to(absDestDir)
54
				)
55
56
			if member.isfile():
57
				parent = absMemberPath.parent
58
				if parent not in createdDirs:
59
					parent.mkdir(parents=True, exist_ok=True)
60
					createdDirs.add(parent)
61
				mode = S_IRWXU | S_IRWXG | S_IRWXO
62
				if not (member.mode & S_IXUSR):
63
					mode &= ~(S_IXUSR | S_IXGRP | S_IXOTH)
64
				fd = osopen(absMemberPath, O_CREAT | O_WRONLY | O_BINARY, mode)
65
				with fdopen(fd, 'wb') as out:
66
					inp = tar.extractfile(member)
67
					bytesLeft = member.size
68
					while bytesLeft > 0:
69
						buf = inp.read(bufSize)
70
						out.write(buf)
71
						bytesLeft -= len(buf)
72
			elif member.isdir():
73
				absMemberPath.mkdir(parents=True, exist_ok=True)
74
				createdDirs.add(absMemberPath)
75
			elif member.issym():
76
				try:
77
					symlink(member.linkname, absMemberPath)
78
				except OSError as ex:
79
					print(
80
						'WARNING: Skipping symlink creation: '
81
						f'{absMemberPath} -> {member.linkname}: {ex}'
82
					)
83
			else:
84
				raise ValueError(
85
					f'Cannot extract tar entry "{member.name}": '
86
					'not a regular file, symlink or directory'
87
				)
88
			# Set file/directory modification time to match the archive.
89
			# For example autotools track dependencies between archived files
90
			# and will attempt to regenerate them if the time stamps indicate
91
			# one is older than the other.
92
			if member.isfile():
93
				utime(absMemberPath, (member.mtime, member.mtime))
94
95
class TopLevelDirRenamer:
96
97
	def __init__(self, newName):
98
		self.newName = newName
99
100
	def __call__(self, oldPath):
101
		return Path(self.newName, *oldPath.parts[1:])
102
103
if __name__ == '__main__':
104
	if 3 <= len(sys.argv) <= 4:
105
		if len(sys.argv) == 4:
106
			renameTopLevelDir = TopLevelDirRenamer(sys.argv[3])
107
		else:
108
			renameTopLevelDir = None
109
		extract(sys.argv[1], sys.argv[2], renameTopLevelDir)
110
	else:
111
		print('Usage: python3 extract.py archive destination [new-top-level-dir]', file=sys.stderr)
112
		sys.exit(2)