blob: 3d3ea5ce155fb5fd672efd1a52abef50d00c95ac [file] [log] [blame]
# Copyright 2018 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
"""Standalone Python script to archive a set of files. Intended to be used by
the 'archive' recipe module internally. Should not be used elsewhere.
"""
# [VPYTHON:BEGIN]
# python_version: "3.11"
# wheel: <
# name: "infra/python/wheels/zstandard/${vpython_platform}"
# version: "version:0.16.0"
# >
# [VPYTHON:END]
from contextlib import contextmanager
import json
import os
import sys
import tarfile
import zipfile
import zstandard
def zip_opener(path):
"""Opens a zipfile to write and adds .name and .add attributes to make it
duck-type compatible with a tarfile.TarFile for the purposes of archive."""
zf = zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True)
zf.name = zf.filename
zf.add = zf.write
return zf
@contextmanager
def tar_zstandard_opener(path):
"""Opens a zstandard-compressed tar file to write."""
ctx = zstandard.ZstdCompressor()
zstd_file = ctx.stream_writer(open(path, 'wb'), closefd=True)
tf = tarfile.open(path, 'w:', fileobj=zstd_file)
try:
yield tf
finally:
tf.close()
zstd_file.close()
def archive(out, root, entries):
"""Archives set of files and directories to `out`.
Works everywhere where python works (Windows and POSIX).
Args:
out: tarfile.TarFile (or duck-type compatible ZipFile)
root: absolute path to a directory that will become a root of the archive.
entries: list of dicts, describing what to tar, see tar/api.py.
Returns:
Exit code (0 on success).
"""
def add(path):
assert path.startswith(root), path
# Do not add itself to archive.
if path == out.name:
return
archive_name = path[len(root):]
print('Adding %s' % archive_name)
out.add(path, archive_name)
for entry in entries:
tp = entry['type']
path = entry['path']
if tp == 'file':
add(path)
elif tp == 'dir':
for cur, _, files in os.walk(path):
for name in files:
add(os.path.join(cur, name))
else:
raise AssertionError('Invalid entry type: %s' % (tp,))
OPENER_FUNCS = {
'tar': lambda path: tarfile.open(path, 'w'),
'tgz': lambda path: tarfile.open(path, 'w|gz'),
'tbz': lambda path: tarfile.open(path, 'w|bz2'),
'tzst': tar_zstandard_opener,
'zip': zip_opener,
}
def main():
# See tar/api.py, def tar(...) for format of |data|.
data = json.load(sys.stdin)
entries = data['entries']
output = data['output']
archive_type = data['archive_type']
root = os.path.join(data['root'], '')
# Archive root directory should exist and be an absolute path.
assert os.path.exists(root), root
assert os.path.isabs(root), root
# Output tar path should be an absolute path.
assert os.path.isabs(output), output
print('Archiving %s -> %s (%s)...' % (root, output, archive_type))
# TODO(iannucci): use CIPD to fetch native clients instead of using python
# builtins.
try:
with OPENER_FUNCS[archive_type](output) as arc:
archive(arc, root, entries)
print('Archive size: %.1f KB' % (os.stat(output).st_size / 1024.0,))
return 0
except:
try:
os.remove(output)
except: # pylint: disable=bare-except
pass
raise
if __name__ == '__main__':
sys.exit(main())