ComfyUI-Ranking-API / hf_snapshot_store.py
ZHIWEI666's picture
3.0更新
c8e5eaa verified
Raw History Blame Contribute Delete
4.39 kB
"""HF私有临时快照生命周期;创建/回收必须持同目录公共提交锁。"""
from contextlib import contextmanager
import json
import logging
import os
from pathlib import Path
import re
import stat
import uuid
logger=logging.getLogger('ComfyUI-Ranking.DB')
class SnapshotStore:
MANIFEST='snapshot-owner.json'
NAME=re.compile(r'\.hf-sync-[0-9a-f]{32}\Z')
PAYLOAD=re.compile(r'(0|[1-9][0-9]*)\.json\Z')
def __init__(self,directory):
self.root=Path(os.path.abspath(directory))
@staticmethod
def _check(path,directory=False):
info=os.lstat(path)
if (getattr(info,'st_file_attributes',0)&1024
or not (stat.S_ISDIR(info.st_mode) if directory else stat.S_ISREG(info.st_mode))
or not info.st_ino):
raise ValueError('invalid snapshot path')
return (info.st_dev,info.st_ino)
def _owner(self,path):
if path.parent!=self.root or not self.NAME.fullmatch(path.name):
raise ValueError('unknown snapshot directory')
self._check(self.root,directory=True)
directory_id=self._check(path,directory=True)
marker=path/self.MANIFEST
marker_id=self._check(marker)
descriptor=os.open(marker,os.O_RDONLY|getattr(os,'O_NOFOLLOW',0)
|getattr(os,'O_NONBLOCK',0)|getattr(os,'O_BINARY',0))
with os.fdopen(descriptor,'rb') as stream:
info=os.fstat(stream.fileno())
if not stat.S_ISREG(info.st_mode) or (info.st_dev,info.st_ino)!=marker_id:
raise ValueError('snapshot owner identity changed')
data=stream.read(4097)
if len(data)>4096:raise ValueError('snapshot owner too large')
owner=json.loads(data)
if (not isinstance(owner,dict) or set(owner)!={'schema','kind','directory'}
or type(owner['schema']) is not int or owner['schema']!=1
or owner['kind']!='ranking-hf-snapshot' or owner['directory']!=path.name):
raise ValueError('invalid snapshot owner')
if self._check(path,directory=True)!=directory_id or self._check(marker)!=marker_id:
raise ValueError('snapshot identity changed')
return directory_id
def remove(self,path):
"""只删除验证归属且内容全为普通快照文件的本层目录,不递归删除。"""
path=Path(path)
directory_id=self._owner(path)
entries=[]
for entry in path.iterdir():
if len(entries)>=4096:raise ValueError('too many snapshot files')
if entry.name!=self.MANIFEST and not self.PAYLOAD.fullmatch(entry.name):
raise ValueError('unknown snapshot content')
self._check(entry)
entries.append(entry)
# 先完整校验,再删除数据,归属记录最后删除,失败可在后续接手。
for entry in sorted(entries,key=lambda entry:entry.name==self.MANIFEST):
self._check(self.root,directory=True)
if self._check(path,directory=True)!=directory_id:
raise ValueError('snapshot directory changed')
entry.unlink()
path.rmdir()
def cleanup_stale(self):
self._check(self.root,directory=True)
removed=retained=0
for path in self.root.glob('.hf-sync-*'):
try:self.remove(path);removed+=1
except FileNotFoundError:
if path.exists() or path.is_symlink():retained+=1
except (OSError,ValueError):retained+=1
return removed,retained
@contextmanager
def temporary(self):
self._check(self.root,directory=True)
path=self.root/('.hf-sync-'+uuid.uuid4().hex)
path.mkdir(mode=0o700)
try:
with (path/self.MANIFEST).open('xb') as stream:
data=json.dumps({'schema':1,'kind':'ranking-hf-snapshot','directory':path.name}).encode('utf-8')
stream.write(data);stream.flush();os.fsync(stream.fileno())
yield str(path)
finally:
try:self.remove(path)
except FileNotFoundError:
if path.exists() or path.is_symlink():
logger.warning('HF snapshot cleanup unavailable; retained for recovery')
except (OSError,ValueError):
logger.warning('HF snapshot cleanup unavailable; retained for recovery')