Add hierarchical geography and cloud-hosted profile images
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""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 until<now:self.cache.pop(k)
|
||||
self.cache[ref['sha256']] = (now+600,raw)
|
||||
self.cache.move_to_end(ref['sha256'])
|
||||
while sum(len(v[1]) for v in self.cache.values())>self.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
|
||||
Reference in New Issue
Block a user