"""Baidu-hosted UI images. DB stores references; only a bounded RAM cache holds bytes.""" import base64 import hashlib import re import secrets import threading import time from collections import OrderedDict from appearance import png_bytes class CloudImages: def __init__(self, app): self.a = app self.guard = threading.RLock() self.cache = OrderedDict() self.limit = 16*1024*1024 def remember(self, ref, raw): with self.guard: now=time.monotonic() for k,(until,_) in list(self.cache.items()): if untilself.limit:self.cache.popitem(last=False) def get(self, ref): with self.guard: found=self.cache.get(ref['sha256']) if found and found[0]>time.monotonic():return found[1] with self.a.BAIDU.open_file('shared',ref) as response: raw=response.read(ref['size']+1) if len(raw)!=ref['size'] or hashlib.sha256(raw).hexdigest()!=ref['sha256']: raise self.a.Problem('网盘图片校验未通过,请重新上传',502) self.remember(ref,raw) return raw def put(self, value, scope, kind, maximum=2*1024*1024): if kind not in ('封面','Logo','图标') or not re.fullmatch(r'[a-f0-9]{16}|platform',scope): raise self.a.Problem('图片归属不正确') try:raw=png_bytes(value,maximum) except ValueError as error:raise self.a.Problem(str(error).replace('Logo',kind)) a=self.a a.BAIDU.token('shared') with a.LOCK: instance=a.setting('cloud_instance') if not instance:instance=secrets.token_hex(8);a.set_setting('cloud_instance',instance) digest=hashlib.sha256(raw).hexdigest() category='平台外观' if scope=='platform' else '家庭与场所' remote='/apps/'+a.BAIDU.public_config()['appFolder']+'/'+category+'/'+instance+'/'+scope+'/'+kind+'/'+digest+'.png' md5=hashlib.md5(raw).hexdigest();hashes=[md5] pre=a.BAIDU.prepare(remote,len(raw),hashes) result=pre.get('existing') if not result: needed=pre.get('block_list',[0]) if not isinstance(needed,list) or any(type(i) is not int or i!=0 for i in needed):raise a.Problem('网盘图片分片信息不正确',502) if 0 in needed:a.BAIDU.send_block(remote,pre['uploadid'],0,raw,md5) result=a.BAIDU.complete(dict(remotePath=remote,size=len(raw),uploadId=pre['uploadid'],hashes=hashes)) if not result.get('fs_id') or result.get('path')!=remote or result.get('size')!=len(raw): raise a.Problem('网盘尚未确认图片,请重试',502) ref=dict(provider='baidu',fsId=str(result['fs_id']),remotePath=remote,size=len(raw),sha256=digest,mime='image/png') # Verify by reading the remote file before committing the reference, including retries. with a.BAIDU.open_file('shared',ref) as response: check=response.read(len(raw)+1) if check!=raw:raise a.Problem('网盘图片回读校验失败,原有封面未改变',502) self.remember(ref,raw) return ref def migrate_legacy(self): """Explicit admin maintenance; replace each reference only after cloud verification.""" a=self.a;result=[] for home in a.objects('households'): value=home.get('coverData') if not value:continue ref=self.put(value,home['id'],'封面') with a.LOCK: current=a.get_object('households',home['id']) if current.get('coverData')!=value:continue current.pop('coverData',None);current['coverRef']=ref current['profileRevision']=current.get('profileRevision',0)+1 current['coverRevision']=ref['sha256'][:16];a.save_object('households',current) result.append('household:'+home['id']) for kind,field in [('Logo','logo'),('图标','icon')]: old=a.APPEARANCE.config();value=old.get(field+'Data') if not value:continue ref=self.put(value,'platform',kind,256*1024) with a.LOCK: current=a.APPEARANCE.config() if current.get(field+'Data')!=value:continue current.pop(field+'Data',None);current[field+'Ref']=ref current['revision']+=1;a.set_setting('appearance',current) result.append('brand:'+field) return result