calamares/src/modules/unpackfs/main.py

251 lines
9.1 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# encoding: utf-8
# === This file is part of Calamares - <http://github.com/calamares> ===
#
# Copyright 2014, Teo Mrnjavac <teo@kde.org>
# Copyright 2014, Daniel Hillenbrand <codeworkx@bbqlinux.org>
# Copyright 2014, Philip Müller <philm@manjaro.org>
#
# Calamares is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Calamares is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Calamares. If not, see <http://www.gnu.org/licenses/>.
import os
import re
2014-07-28 17:46:56 +02:00
import shutil
import subprocess
import sys
import tempfile
2014-07-28 12:17:06 +02:00
from collections import namedtuple
from libcalamares import *
class UnpackEntry:
__slots__= ['source', 'sourcefs', 'destination', 'copied', 'total']
def __init__(self, source, sourcefs, destination):
self.source = source
self.sourcefs = sourcefs
self.destination = destination
self.copied = 0
self.total = 0
ON_POSIX = 'posix' in sys.builtin_module_names
def list_excludes(destination):
prefix = destination.replace('//', '/')
if not prefix.endswith('/'):
prefix = prefix + '/'
lst = []
for line in open('/etc/mtab').readlines():
device, mount_point, _ = line.split(" ", 2)
if mount_point.startswith(prefix):
# -1 to include the leading / from the end of the prefix
# also add a trailing /
lst.extend(['--exclude', mount_point[len(prefix)-1:] + '/'])
return lst
2014-07-29 15:10:18 +02:00
def file_copy(source, dest, progress_cb):
# Environment used for executing rsync properly
# Setting locale to C (fix issue with tr_TR locale)
at_env = os.environ
at_env["LC_ALL"] = "C"
2014-08-01 11:46:29 +02:00
# `source` *must* end with '/' otherwise a directory named after the source
# will be created in `dest`: ie if `source` is "/foo/bar" and `dest` is
# "/dest", then files will be copied in "/dest/bar".
source += "/"
args = ['rsync', '-aHAXr']
args.extend(list_excludes(dest))
args.extend(['--progress', source, dest])
process = subprocess.Popen(args,
2014-07-29 15:10:18 +02:00
env=at_env,
bufsize=1,
stdout=subprocess.PIPE,
close_fds=ON_POSIX)
2014-07-29 15:10:18 +02:00
for line in iter(process.stdout.readline, b''):
# small comment on this regexp.
# rsync outputs three parameters in the progress.
# xfer#x => i try to interpret it as 'file copy try no. x'
# to-check=x/y, where:
# - x = number of files yet to be checked
# - y = currently calculated total number of files.
2014-07-29 15:10:18 +02:00
# but if you're copying directory with some links in it, the xfer#
# might not be a reliable counter (for one increase of xfer, many
# files may be created).
# In case of manjaro, we pre-compute the total number of files.
2014-07-29 15:10:18 +02:00
# therefore we can easily subtract x from y in order to get real files
# copied / processed count.
m = re.findall(r'xfr#(\d+), ir-chk=(\d+)/(\d+)', line.decode())
if m:
# we've got a percentage update
num_files_remaining = int(m[0][1])
num_files_total_local = int(m[0][2])
# adjusting the offset so that progressbar can be continuesly drawn
num_files_copied = num_files_total_local - num_files_remaining
# I guess we're updating every 100 files...
if num_files_copied % 100 == 0:
2014-07-29 15:10:18 +02:00
progress_cb(num_files_copied)
process.wait()
if process.returncode != 0:
return "rsync failed with error code {}.".format(process.returncode)
return None
class UnpackOperation:
2014-07-29 15:10:18 +02:00
def __init__(self, entries):
self.entries = entries
self.entry_for_source = dict((x.source, x) for x in self.entries)
2014-07-29 15:10:18 +02:00
def report_progress(self):
progress = float(0)
for entry in self.entries:
if entry.total == 0:
continue
2014-07-29 15:10:18 +02:00
partialprogress = 0.05 # Having a total !=0 gives 5%
2014-07-29 15:10:18 +02:00
partialprogress += 0.95 * \
(entry.copied / float(entry.total))
progress += partialprogress / len(self.entries)
2014-07-29 15:10:18 +02:00
job.setprogress(progress)
2014-07-29 15:10:18 +02:00
def run(self):
source_mount_path = tempfile.mkdtemp()
try:
for entry in self.entries:
2014-07-29 15:10:18 +02:00
imgbasename = os.path.splitext(
os.path.basename(entry.source))[0]
2014-07-30 15:06:59 +02:00
imgmountdir = os.path.join(source_mount_path, imgbasename)
2014-07-29 15:10:18 +02:00
os.mkdir(imgmountdir)
self.mount_image(entry, imgmountdir)
2014-08-19 19:12:48 +02:00
fslist = ""
if entry.sourcefs == "squashfs":
if shutil.which("unsquashfs") == None:
2014-10-15 12:51:43 +02:00
return ("Failed to unpack image", "Failed to find unsquashfs, make sure you have "
"the squashfs-tools package installed")
2014-08-19 19:12:48 +02:00
fslist = subprocess.check_output(["unsquashfs",
"-l",
entry.source])
if entry.sourcefs == "ext4":
2014-08-19 12:07:17 +02:00
fslist = subprocess.check_output(["find",
imgmountdir,
"-type", "f"])
2014-08-19 19:12:48 +02:00
entry.total = len(fslist.splitlines())
2014-07-29 15:10:18 +02:00
self.report_progress()
error_msg = self.unpack_image(entry, imgmountdir)
if error_msg:
return ("Failed to unpack image {}".format(entry.source),
error_msg)
return None
finally:
2014-07-29 15:10:18 +02:00
shutil.rmtree(source_mount_path)
2014-07-28 12:17:06 +02:00
def mount_image(self, entry, imgmountdir):
subprocess.check_call(["mount",
entry.source,
imgmountdir,
2014-08-19 12:02:03 +02:00
"-t",
entry.sourcefs,
"-o", "loop"])
def unpack_image(self, entry, imgmountdir):
2014-07-30 15:35:51 +02:00
def progress_cb(copied):
entry.copied = copied
2014-07-30 15:35:51 +02:00
self.report_progress()
try:
return file_copy(imgmountdir,
2014-07-29 15:10:18 +02:00
entry.destination,
2014-07-30 15:35:51 +02:00
progress_cb)
finally:
2014-07-30 15:06:39 +02:00
subprocess.check_call(["umount", "-l", imgmountdir])
def run():
2014-07-29 15:10:18 +02:00
# from globalstorage: rootMountPoint
# from job.configuration:
# the path to where to mount the source image(s) for copying
# an ordered list of unpack mappings for image file <-> target dir relative
# to rootMountPoint, e.g.:
# configuration:
# unpack:
# - source: "/path/to/filesystem.img"
# sourcefs: "ext4"
# destination: ""
# - source: "/path/to/another/filesystem.sqfs"
# sourcefs: "squashfs"
# destination: ""
PATH_PROCFS = '/proc/filesystems'
2014-07-29 15:10:18 +02:00
root_mount_point = globalstorage.value("rootMountPoint")
if not root_mount_point:
return ("No mount point for root partition in globalstorage",
"globalstorage does not contain a \"rootMountPoint\" key, "
"doing nothing")
if not os.path.exists(root_mount_point):
return ("Bad mount point for root partition in globalstorage",
"globalstorage[\"rootMountPoint\"] is \"{}\", which does not "
"exist, doing nothing".format(root_mount_point))
unpack = list()
2014-07-29 15:10:18 +02:00
for entry in job.configuration["unpack"]:
source = os.path.abspath(entry["source"])
2014-08-19 12:02:03 +02:00
sourcefs = entry["sourcefs"]
# Get supported filesystems
fs_is_supported = False
if os.path.isfile(PATH_PROCFS) and os.access(PATH_PROCFS, os.R_OK):
procfile = open(PATH_PROCFS, 'r')
filesystems = procfile.read()
procfile.close
filesystems = filesystems.replace("nodev", "")
filesystems = filesystems.replace("\t", "")
filesystems = filesystems.splitlines()
# Check if the source filesystem is supported
for fs in filesystems:
if fs == sourcefs:
fs_is_supported = True
if fs_is_supported == False:
2014-08-19 12:02:03 +02:00
return "Bad filesystem", "sourcefs=\"{}\"".format(sourcefs)
destination = os.path.abspath(root_mount_point + entry["destination"])
if not os.path.exists(source) or os.path.isdir(source):
2014-07-30 15:05:44 +02:00
return ("Bad source", "source=\"{}\"".format(source))
if not os.path.isdir(destination):
return ("Bad destination",
"destination=\"{}\"".format(destination))
unpack.append(UnpackEntry(source, sourcefs, destination))
unpackop = UnpackOperation(unpack)
return unpackop.run()