# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-## Copyright 2015-2021,2024 Canonical Ltd.## This program is free software; you can redistribute it and/or# modify it under the terms of the GNU Lesser General Public# License version 3 as published by the Free Software Foundation.## This program 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# Lesser General Public License for more details.## You should have received a copy of the GNU Lesser General Public License# along with this program. If not, see <http://www.gnu.org/licenses/>."""Handle part files organization.Installed part files can be reorganized according to a mapping specifiedunder the `organize` entry in a part definition. In the key/value pair,the key represents the path of a file inside the part and the valuerepresents how the file is going to be staged."""importcontextlibimportosimportshutilfromcollections.abcimportMappingfromglobimportiglobfrompathlibimportPathfromcraft_partsimporterrorsfromcraft_parts.utilsimportfile_utils,path_utils
[docs]deforganize_files(*,part_name:str,file_map:dict[str,str],install_dir_map:Mapping[str|None,Path],overwrite:bool,)->None:"""Rearrange files for part staging. If partitions are enabled, source filepaths must be in the default partition. :param part_name: The name of the part to organize files for. :param file_map: A mapping of source filepaths to destination filepaths. :param install_dir_map: A mapping of partition names to their install directories. :param overwrite: Whether existing files should be overwritten. This is only used in build updates, when a part may organize over files it previously organized. :raises FileOrganizeError: If the destination file already exists or multiple files are organized to the same destination. :raises FileOrganizeError: If partitions are enabled and the source file is not from the default partition. """forkeyinsorted(file_map,key=lambdax:["*"inx,x]):src_partition,src_inner_path=path_utils.get_partition_and_path(key)ifsrc_partitionandsrc_partition!="default":raiseerrors.FileOrganizeError(part_name=part_name,message=(f"Cannot organize files from {src_partition!r} partition. ""Files can only be organized from the 'default' partition"),)src=os.path.join(install_dir_map[src_partition],src_inner_path)# Remove the leading slash so the path actually joins# Also trailing slash is significant, be careful if using pathlib!dst_partition,dst_inner_path=path_utils.get_partition_and_path(file_map[key].lstrip("/"))dst=os.path.join(install_dir_map[dst_partition],dst_inner_path)# prefix the partition to the log-friendly version of the destinationifdst_partitionanddst_partition!="default":dst_string=f"({dst_partition})/{dst_inner_path}"else:dst_string=str(dst_inner_path)sources=iglob(src,recursive=True)# Keep track of the number of glob expansions so we can properly error if more# than one tries to organize to the same filesrc_count=0forsrcinsources:src_count+=1ifos.path.isdir(src)and"*"notinkey:file_utils.link_or_copy_tree(src,dst)shutil.rmtree(src)continueifos.path.isfile(dst):ifoverwriteandsrc_count<=1:withcontextlib.suppress(FileNotFoundError):os.remove(dst)elifsrc_count>1:raiseerrors.FileOrganizeError(part_name=part_name,message=("multiple files to be organized into "f"{dst_string!r}. If this is ""supposed to be a directory, end it with a slash."),)else:raiseerrors.FileOrganizeError(part_name=part_name,message=(f"trying to organize file {key!r} to "f"{file_map[key]!r}, but "f"{dst_string!r} already exists"),)ifos.path.isdir(dst)andoverwrite:real_dst=os.path.join(dst,os.path.basename(src))ifos.path.isdir(real_dst):shutil.rmtree(real_dst)else:withcontextlib.suppress(FileNotFoundError):os.remove(real_dst)os.makedirs(os.path.dirname(dst),exist_ok=True)shutil.move(src,dst)