blob: 79d633135be8e0ae07937ee2d1c68d1c8449aed9 [file] [log] [blame]
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Standalone python script to zip a set of files. Intended to be used by 'zip'
recipe module internally. Should not be used elsewhere.
"""
import json
import hashlib
import os
import subprocess
import sys
import zipfile
def zip_with_subprocess(root, output, entries):
"""Zips set of files and directories using 'zip' utility.
Works only on Linux and Mac, uses system 'zip' program.
Args:
root: absolute path to a directory that will become a root of the archive.
output: absolute path to a destination archive.
entries: list of dicts, describing what to zip, see zip/api.py.
Returns:
Exit code (0 on success).
"""
# Collect paths relative to |root| of all items we'd like to zip.
items_to_zip = []
for entry in entries:
tp = entry['type']
path = entry['path']
if tp == 'file':
# File must exist and be inside |root|.
assert os.path.isfile(path), path
assert path.startswith(root), path
file_path = path[len(root):]
hash_file(file_path)
items_to_zip.append(file_path)
elif entry['type'] == 'dir':
# Append trailing '/'.
path = path.rstrip(os.path.sep) + os.path.sep
# Directory must exist and be inside |root| or be |root| itself.
assert os.path.isdir(path), path
assert path.startswith(root), path
dir_path = path[len(root):] or '.'
walk_dir_and_do(path, lambda p: hash_file(p))
items_to_zip.append(dir_path)
else:
raise AssertionError('Invalid entry type: %s' % (tp,))
# Invoke 'zip' in |root| directory, passing all relative paths via stdin.
proc = subprocess.Popen(
args=['zip', '-1', '--recurse-paths', '--symlinks', '-@', output],
stdin=subprocess.PIPE,
cwd=root)
items_to_zip_bytes = []
for item in items_to_zip:
items_to_zip_bytes.append(item if isinstance(item, bytes) else bytes(item, 'UTF-8'))
proc.communicate(b'\n'.join(items_to_zip_bytes))
return proc.returncode
def walk_dir_and_do(directory_path, callback):
for cur, _, files in os.walk(directory_path):
for name in files:
callback(os.path.join(cur, name))
def hash_file(file_path):
BUFFER_SIZE = 1 << 16 # 64kb
sha = hashlib.sha256()
with open(file_path, 'rb') as f:
data = f.read(BUFFER_SIZE)
while data:
sha.update(data)
data = f.read(BUFFER_SIZE)
digest = sha.hexdigest()
print('sha256 digest for %s is:\n%s\n' % (file_path, digest))
def zip_with_python(root, output, entries):
"""Zips set of files and directories using 'zipfile' python module.
Works everywhere where python works (Windows and Posix).
Args:
root: absolute path to a directory that will become a root of the archive.
output: absolute path to a destination archive.
entries: list of dicts, describing what to zip, see zip/api.py.
Returns:
Exit code (0 on success).
"""
with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED,
allowZip64=True) as zip_file:
def add(path, archive_name):
assert path.startswith(root), path
# Do not add itself to archive.
if path == output:
return
if archive_name is None:
archive_name = path[len(root):]
print('Adding %s' % archive_name)
hash_file(path)
zip_file.write(path, archive_name)
for entry in entries:
tp = entry['type']
path = entry['path']
if tp == 'file':
add(path, entry.get('archive_name'))
elif tp == 'dir':
walk_dir_and_do(path, lambda p: add(p, None))
else:
raise AssertionError('Invalid entry type: %s' % (tp,))
return 0
def use_python_zip(entries):
if sys.platform == 'win32':
return True
for entry in entries:
if entry.get('archive_name') is not None:
return True
return False
def main():
# See zip/api.py, def zip(...) for format of |data|.
data = json.load(sys.stdin)
entries = data['entries']
output = data['output']
root = data['root'].rstrip(os.path.sep) + os.path.sep
# Archive root directory should exist and be an absolute path.
assert os.path.exists(root), root
assert os.path.isabs(root), root
# Output zip path should be an absolute path.
assert os.path.isabs(output), output
print('Zipping %s...' % output)
exit_code = -1
try:
if use_python_zip(entries):
# Used on Windows, since there's no builtin 'zip' utility there, and when
# an explicit archive_name is set, since there's no way to do that with
# the native zip utility without filesystem shenanigans
exit_code = zip_with_python(root, output, entries)
else:
# On mac and linux 'zip' utility handles symlink and file modes.
exit_code = zip_with_subprocess(root, output, entries)
finally:
# On non-zero exit code or on unexpected exception, clean up.
if exit_code:
try:
os.remove(output)
except: # pylint: disable=bare-except
pass
if not exit_code:
print('Archive size: %.1f KB' % (os.stat(output).st_size / 1024.0,))
return exit_code
if __name__ == '__main__':
sys.exit(main())