Chronaxis用户手册English
本页目录

chronaxis.results.export_files

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""用有限缓冲写新导出目录,全部文件真正关闭后才独占发布。23供信号CSV及新报文导出共用:输入是已编码的有限bytes,输出为按模式返回的目录或实际路径清单。4临时文件保留供排查;失败不覆盖用户目录、不删除已有数据。累计spool先预约,5RAM按自有槽释放;关闭失败保真实句柄供重试。文件和目录均使用平台不覆盖移动;不支持的环境明确拒绝。此层不读取原始日志、不解码,也不计算插值。6"""78from hashlib import sha2569import json10from pathlib import Path11import platform12from uuid import uuid41314from .budget import ResourceBudget, _reserve_owned_ram, _release_owned_ram, _cleanup_owner15from .errors import ResultClosedError, ResultFormatError161718class ExportFiles:19    """拥有当前/挂起的有限输出文件、消费游标及可选处理owner,最多16文件。"""2021    def __init__(self, destination, budget, *, integrity=True, timestamped=False):22        """预检输出并创建独占临时目录。参数: destination输出根、budget共享账本,integrity控制摘要,timestamped为内部选择的公开时间命名模式。2324        返回: owner。异常: 路径/平台/预算/I/O错;清理失败附export_cleanup_owner。25        """26        if not isinstance(destination,(str,Path)) or not isinstance(budget,ResourceBudget):27            raise TypeError('导出需要目录路径和ResourceBudget')28        if type(integrity) is not bool:29            raise TypeError('integrity必须为bool')30        self._integrity=integrity31        self._timestamped = timestamped32        self._failed = False33        self._temporary_identity = None34        self.destination = Path(destination).absolute()35        # 只复用符合规则的结果根;已有文件仍通过独占命名保护。36        from .platform_files import require_publication_platform37        require_publication_platform()38        try:39            self.destination.lstat()40        except FileNotFoundError:41            pass42        else:43            if not timestamped or not self.destination.is_dir() or self.destination.is_symlink():44                raise FileExistsError(self.destination)45        self._budget = budget46        self._held_cost = self._fixed_cost = self._row_cost = 047        self._closed = False48        self.reader = self.worker = self.stream = self.verifier = self.temporary = None49        self._parked = {}50        self._name = self._hash = None51        self._bytes = 052        self.files = []53        try:54            _reserve_owned_ram(self,131072+len(str(self.destination).encode('utf-8'))*32,'_fixed_cost')55            self.destination.parent.mkdir(parents=True,exist_ok=True)56            self.temporary = self.destination.parent/('.'+self.destination.name+'.'+uuid4().hex+'.incomplete')57            self.temporary.mkdir(exist_ok=False)58            from .owned_directory import directory_identity59            self._temporary_identity = directory_identity(self.temporary)60        except BaseException as error:61            self._failed = True62            _cleanup_owner(self,error,'export_cleanup_owner','导出目录构造')63            raise6465    @property66    def closed(self):67        """返回真实关闭状态。参数: 无。返回: bool。异常: 无。68        """69        return self._closed7071    def open_file(self, name):72        """打开一个固定平面文件。参数: name安全文件名。返回: None。异常: 未关闭/重复/非法名/I/O错。73        """74        if self._closed or self.stream is not None:75            raise ResultClosedError('导出已关闭或前一文件未关闭')76        if (type(name) is not str or not name or len(name)>128 or Path(name).name!=name77                or any(c in name for c in '/\\:') or name in ('.','..') or len(self.files)+len(self._parked)>=16):78            raise ValueError('导出文件名或数量非法')79        if name in self._parked or any(item['name']==name for item in self.files):80            raise ValueError('导出文件重复')81        # 多张表共用同次输入时会同时存在缓冲;每个新文件单独预约,不各自借满总预算。82        _reserve_owned_ram(self,131072,'_fixed_cost')83        self.stream = (self.temporary/name).open('xb',buffering=65536)84        self._name,self._hash,self._bytes = name,sha256() if self._integrity else None,08586    def select_file(self, name):87        """切换有限输出表,不重新打开或覆盖。参数: name已开或新固定文件名。返回: None。异常: 打开/状态错。88        """89        if self._closed:90            raise ResultClosedError('导出已关闭')91        if self._name == name:92            return93        if self.stream is not None:94            self._parked[self._name] = (self.stream,self._hash,self._bytes)95            self.stream = self._name = self._hash = None96        if name in self._parked:97            self.stream,self._hash,self._bytes = self._parked.pop(name)98            self._name = name99        else:100            self.open_file(name)101102    def append(self, payload):103        """有限块实际写入并更新摘要。参数: payload最多64KiB bytes。返回: None。异常: 配额/写入/结构错。104        """105        if self._closed or self.stream is None:106            raise ResultClosedError('导出文件未打开')107        if type(payload) is not bytes or len(payload)>65536:108            raise ResultFormatError('导出块需要至多64KiB bytes')109        # 配额在任何写动作之前预约,短写继续;零进展不能伪装成功。110        self._budget.reserve_spool(len(payload))111        position = 0112        while position < len(payload):113            written = self.stream.write(payload[position:])114            if type(written) is not int or not 0 < written <= len(payload)-position:115                raise OSError('导出写入没有合法进展')116            position += written117        if self._hash is not None: self._hash.update(payload)118        self._bytes += len(payload)119120    def close_file(self):121        """真实关闭后登记摘要。参数: 无。返回: None。异常: 关闭失败保留句柄供重试。122        """123        if self.stream is None:124            return125        self.stream.close()126        self.stream = None127        self.files.append(dict(name=self._name,bytes=self._bytes,128            **({} if self._hash is None else {'sha256':self._hash.hexdigest()})))129        self._name = self._hash = None130131    def close_reader(self):132        """关闭已接管的输入游标。参数: 无。返回: None。异常: 原关闭错误,保留游标供重试。133        """134        if self.reader is not None:135            close = getattr(self.reader,'close',None)136            if close is not None:137                close()138            self.reader = None139140    def close_files(self):141        """关闭当前及挂起文件。参数: 无。返回: None。异常: 原关闭错误,未关的仍在owner中。142        """143        self.close_file()144        while self._parked:145            name = next(iter(self._parked))146            self.select_file(name)147            self.close_file()148149    def register_external(self,name):150        """登记同目录内已关闭的外部writer文件。参数: name固定文件名。返回: None。异常: 名称/读取/关闭错。151152        外部writer已付spool,本层仅有限回读计算摘要,不再次计算磁盘费用。153        """154        if (self._closed or self.verifier is not None or type(name) is not str or not name or len(name)>128 or Path(name).name!=name155                or any(c in name for c in '/\\:') or name in ('.','..')156                or len(self.files)+len(self._parked)+(self.stream is not None)>=16157                or name==self._name or name in self._parked or any(f['name']==name for f in self.files)):158            raise ValueError('外部导出文件名称或状态非法')159        # 未请求完整性诊断时,只登记外部writer已关闭文件的实际大小,不为摘要回读。160        if not self._integrity:161            self.files.append({'name':name,'bytes':(self.temporary/name).stat().st_size})162            return163        self.verifier=(self.temporary/name).open('rb')164        digest=sha256(); size=0165        while True:166            chunk=self.verifier.read(65536)167            if not chunk:168                break169            digest.update(chunk); size+=len(chunk)170        self.verifier.close()171        self.verifier=None172        self.files.append({'name':name,'sha256':digest.hexdigest(),'bytes':size})173174    def finish(self, metadata):175        """写完成清单并原子安装目录。参数: metadata有界JSON事实。返回: 内部目录Path或公开ExportManifest。176177        异常: 序列化/配额/关闭/竞态目标错误;失败保.incomplete目录,不冒报发布成功。178        """179        from ..execution.history import _metadata_bytes180        # 输入完整消费并真实关闭、数据文件关闭后才写完成清单。181        self.close_reader()182        if self.worker is not None:183            self.worker.close()184            self.worker = None185        if self.verifier is not None:186            self.verifier.close()187            self.verifier=None188        self.close_files()189        publication = None190        result = None191        try:192            # 内部导出仍使用新目录;公开调用在已有根下使用同次时间文件名。193            if self._timestamped:194                from .publication import PublicationNames195                publication = PublicationNames(self.destination, (*[f['name'] for f in self.files], 'manifest.json'))196                metadata = dict(metadata)197                if 'exact_source_index' in metadata:198                    metadata['exact_source_index'] = publication.names[metadata['exact_source_index']]199            _reserve_owned_ram(self,65536+_metadata_bytes(metadata)*16,'_row_cost')200            try:201                files = tuple(dict(f, name=publication.names[f['name']]) if publication else dict(f) for f in self.files)202                document = dict(metadata,export_version=1,status='completed',files=files)203                payload = (json.dumps(document,ensure_ascii=False,allow_nan=False,sort_keys=True)+'\n').encode('utf-8')204                self.open_file('manifest.json')205                # 清单受声明规模和RAM预算约束,按有限块写出。206                for start in range(0,len(payload),65536):207                    self.append(payload[start:start+65536])208                self.close_file()209            finally:210                _release_owned_ram(self,self._row_cost,'_row_cost')211            self.close()212            if publication is None:213                from .platform_files import move_no_replace214                move_no_replace(self.temporary,self.destination)215                return self.destination216            from .manifest import ExportManifest217            result = ExportManifest(self.destination, self.destination/publication.names['manifest.json'],218                                    tuple(self.destination/f['name'] for f in files))219            publication.publish(self.temporary, 'manifest.json')220            self.temporary.rmdir()221            return result222        except BaseException as error:223            if publication is not None and publication.completed:224                error.completed_manifest = result225            if publication is not None:226                try:227                    publication.close()228                except BaseException as cleanup:229                    error.add_note('导出命名锁释放失败:' + str(cleanup))230            raise231232    def close(self):233        """只关闭自有资源,保留诊断文件。参数: 无。返回: None。异常: 原关闭/释放错,允许重试。234        """235        if self._closed:236            try:237                self._cleanup_failed_output()238            except BaseException:239                self._closed = False240                raise241            return242        self.close_reader()243        if self.worker is not None:244            self.worker.close()245            self.worker = None246        if self.verifier is not None:247            self.verifier.close()248            self.verifier=None249        self.close_files()250        _release_owned_ram(self,self._row_cost,'_row_cost')251        _release_owned_ram(self,self._fixed_cost,'_fixed_cost')252        self._cleanup_failed_output()253        self._closed = True254255    def _cleanup_failed_output(self):256        """公开导出失败时清理本次临时目录。参数: 无。返回: None。异常: 身份或IO错误,可重试close。"""257        if self._failed and self._timestamped and self.temporary is not None:258            from .owned_directory import remove_owned_directory259            remove_owned_directory(self.temporary, self._temporary_identity)260
Chronaxis 用户手册 · 离线阅读