Chronaxis用户手册English
本页目录

chronaxis.results.publication

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""给同次输出分配时间文件名,并在已有结果目录中不覆盖地发布。23只检查本次候选路径,不扫描旧结果。平台不覆盖移动保护竞争期间出现的文件;4临时锁仅协调使用相同时间后缀的发布者,完成清单最后移动。失败回滚本次移动,5身份发生变化的路径保留给调用者处理,不删除其他运行或外部替换的文件。6"""78from datetime import datetime9import os10from pathlib import Path11from secrets import randbits12from .platform_files import move_no_replace, require_publication_platform13from .owned_directory import directory_identity141516def output_root(value):17    """检查可复用结果根。参数: value路径。返回: 绝对Path。异常: 路径/平台错误。"""18    root = Path(value).absolute()19    require_publication_platform()20    if root.is_symlink() or root.is_junction() or (root.exists() and not root.is_dir()):21        raise FileExistsError('输出目标必须为目录:' + str(root))22    return root232425def _identity(path):26    """取得文件身份。参数: path实际路径。返回: 设备和inode。异常: 原stat错误。"""27    value = path.lstat()28    return value.st_dev, value.st_ino293031class PublicationNames:32    """独占本次时间后缀,保留有限文件映射及已移动文件身份。"""3334    def __init__(self, root, names):35        """分配不冲突名称。参数: root输出根、names逻辑文件名。返回: 实例。异常: IO错。"""36        self.root = output_root(root)37        self.root.mkdir(parents=True, exist_ok=True)38        self._root_identity=directory_identity(self.root)39        self.lock = None40        self.moved = []41        self.completed = False42        names = tuple(names)43        if any(Path(n).name != n or n in ('.', '..') for n in names):44            raise ValueError('发布名称必须是单层文件或目录名')45        base = datetime.now().astimezone().strftime('%Y%m%d_%H%M%S_%f')46        index = 047        permission_retry = False48        # 同刻、时钟回拨及已有输出均只影响本组候选,不遍历目录。49        while True:50            stamp = base if index == 0 else f'{base}_{index}'51            lock = self.root / ('.publishing-' + stamp)52            try:53                stream = lock.open('xb')54            except FileExistsError:55                index += 156                continue57            except PermissionError:58                # Windows正在删除同刻锁时也会返回拒绝访问。只换一次独立候选;59                # 真正的目录权限错误仍立即传播,不能无限重试或删除别人的锁。60                if os.name!='nt' or permission_retry:61                    raise62                index = randbits(64) + 163                permission_retry = True64                continue65            except BaseException:66                self.close()67                raise68            with stream:69                value = os.fstat(stream.fileno())70                self.lock = lock71                self.lock_identity = value.st_dev, value.st_ino72            try:73                self.names = {n: f'{Path(n).stem}_{stamp}{Path(n).suffix}' for n in names}74                if any(os.path.lexists(self.root / n) for n in self.names.values()):75                    self.close()76                    index += 177                    continue78            except BaseException as error:79                try:80                    self.close()81                except BaseException as cleanup:82                    error.add_note('命名锁释放失败:' + str(cleanup))83                raise84            self.stamp = stamp85            return8687    def publish(self, temporary, last, installer=None, *, only=None):88        """先移数据再移清单。参数: temporary临时目录、last完成文件、installer可选rename、only本次子集。返回: None。异常: IO/取消。"""89        move = move_no_replace if installer is None else installer90        if directory_identity(self.root)!=self._root_identity:91            raise ValueError('发布根目录身份已变化')92        temporary_identity=directory_identity(Path(temporary))93        # 同一报告再次写出时,既有成功移动不属于此次失败的回滚范围。94        self.moved = []95        self.completed = False96        selected = tuple(self.names) if only is None else tuple(only)97        if last not in selected or any(name not in self.names for name in selected):98            raise ValueError('发布子集必须属于本次命名并包含完成清单')99        try:100            for name in (*[n for n in selected if n != last], last):101                if directory_identity(Path(temporary))!=temporary_identity or directory_identity(self.root)!=self._root_identity:102                    raise ValueError('发布父目录身份已变化')103                source, target = Path(temporary) / name, self.root / self.names[name]104                identity = _identity(source)105                # 先登记候选身份,覆盖系统调用已完成却在返回前取消的情况。106                self.moved.append((source, target, identity))107                move(source, target)108                if _identity(target)!=identity:109                    raise ValueError('移动后的发布对象身份已变化')110            # 释放失败也进入回滚,不能留下completed却报告本次没有发布。111            self.close()112            self.completed = True113        except BaseException as error:114            # 回滚只接管身份仍与移动前一致的对象;其他文件绝不覆盖。115            for source, target, identity in reversed(self.moved):116                rollback_started = False117                try:118                    if _identity(target) == identity:119                        rollback_started = True120                        move_no_replace(target, source)121                    elif target.name == self.names[last]:122                        # 外部替换的清单可能仍引用本次数据;不能删除其依赖。123                        error.add_note('完成清单已被替换,保留已发布文件供调用者核对')124                        break125                except BaseException as cleanup:126                    if not rollback_started and isinstance(cleanup, FileNotFoundError):127                        continue128                    error.add_note('发布回滚未完成:' + str(cleanup))129                    if target.name == self.names[last]:130                        # rename可能已完成,随后目录fd关闭才报错;按实际文件身份判断。131                        try:132                            try:133                                target_identity = _identity(target)134                            except FileNotFoundError:135                                target_identity = None136                            try:137                                source_identity = _identity(source)138                            except FileNotFoundError:139                                source_identity = None140                        except BaseException as inspection:141                            error.add_note('无法确认完成清单位置,保留数据:' + str(inspection))142                            break143                        if target_identity is None and source_identity == identity:144                            error.add_note('完成清单已撤回临时目录,继续撤回其数据文件')145                            continue146                        self.completed = target_identity == identity147                        if self.completed:148                            error.add_note('完成清单仍已发布,其数据文件完整保留')149                        else:150                            error.add_note('完成清单位置或身份已变化,保留数据供核对')151                        break152            try:153                self.close()154            except BaseException as cleanup:155                error.add_note('发布锁释放失败:' + str(cleanup))156            raise157158    def close(self):159        """释放本次命名锁。参数: 无。返回: None。异常: IO错,不删除其他身份的文件。"""160        if self.lock is not None:161            try:162                if _identity(self.lock) == self.lock_identity:163                    self.lock.unlink()164            except FileNotFoundError:165                pass166            self.lock = None167
Chronaxis 用户手册 · 离线阅读