feat: add private journals and centralized Baidu media storage for v0.1.28

This commit is contained in:
Codex
2026-10-04 19:22:31 +08:00
parent 61968adf94
commit 67709832c5
21 changed files with 1187 additions and 35 deletions
+78 -2
View File
@@ -36,6 +36,8 @@ import inventory
import heritage
import appearance
import backups
import baidu
import journal
import ptz
ROOT = Path(__file__).resolve().parent
@@ -83,6 +85,8 @@ INVENTORY = inventory.Inventory(sys.modules[__name__])
HERITAGE = heritage.Heritage(sys.modules[__name__])
APPEARANCE = appearance.Appearance(sys.modules[__name__])
BACKUPS = backups.Backups(sys.modules[__name__])
BAIDU = baidu.Baidu(sys.modules[__name__])
JOURNAL = journal.Journal(sys.modules[__name__])
def setting(key, default=None):
@@ -629,15 +633,28 @@ class Handler(BaseHTTPRequestHandler):
def do_POST(self):
try:
path = url.urlsplit(self.path).path
if path == '/api/journal/chunk':
self.validate_origin(); self.require_auth()
query = dict(url.parse_qsl(url.urlsplit(self.path).query))
try:
size = int(self.headers.get('Content-Length', '0')); offset = int(query.get('offset', '-1'))
except ValueError: raise Problem('上传分片参数不正确')
if not 0 < size <= journal.CHUNK: raise Problem('上传分片大小不正确')
self.connection.settimeout(45)
raw = self.rfile.read(size)
if len(raw) != size: raise Problem('分片未完整接收,请重试')
self.answer(JOURNAL.chunk(self.user, query.get('id'), offset, raw))
return
# 12,000 Chinese characters plus JSON escapes and linked people exceed 16 KiB.
data = self.body(3*1024*1024 if path in ('/api/appearance','/api/households') else 131072 if path in ('/api/family/event','/api/heritage') else 16384)
data = self.body(3*1024*1024 if path in ('/api/appearance','/api/households') else 131072 if path in ('/api/family/event','/api/heritage','/api/journal') else 16384)
self.validate_origin()
if path in ('/api/login', '/api/setup'):
self.login(data, path == '/api/setup')
return
self.require_auth()
if path not in ('/api/logout', '/api/account/password', '/api/preferences', '/api/family/restore',
'/api/family/person', '/api/family/link', '/api/family/event', '/api/heritage', '/api/ptz', '/api/ptz/presets'):
'/api/family/person', '/api/family/link', '/api/family/event', '/api/heritage', '/api/ptz', '/api/ptz/presets',
'/api/journal', '/api/journal/action', '/api/journal/upload', '/api/journal/finish', '/api/journal/remove-upload'):
self.require_manager()
if path == '/api/logout':
with LOCK:
@@ -646,6 +663,24 @@ class Handler(BaseHTTPRequestHandler):
self.answer({'ok': True}, cookie='vision=; Path=/; Max-Age=0; HttpOnly; SameSite=Strict')
elif path == '/api/preferences':
self.answer(APPEARANCE.save_preferences(data, self.user))
elif path == '/api/baidu/config':
self.require_admin(); self.answer(BAIDU.configure(data, self.user))
elif path == '/api/baidu/connect':
self.require_admin()
result, cookie = BAIDU.begin(self.user, 'media', self.token(), self.headers.get('Host'))
self.answer(result, cookie=cookie)
elif path == '/api/baidu/disconnect':
self.require_admin(); self.answer(BAIDU.disconnect(self.user, 'media'))
elif path == '/api/journal':
self.answer(JOURNAL.save(self.user, data))
elif path == '/api/journal/action':
self.answer(JOURNAL.action(self.user, data))
elif path == '/api/journal/upload':
self.answer(JOURNAL.begin_upload(self.user, data))
elif path == '/api/journal/finish':
self.answer(JOURNAL.finish_upload(self.user, data.get('id')))
elif path == '/api/journal/remove-upload':
self.answer(JOURNAL.remove_upload(self.user, data.get('id')))
elif path == '/api/appearance':
self.require_admin()
self.answer(APPEARANCE.save_config(data, self.user))
@@ -841,6 +876,17 @@ class Handler(BaseHTTPRequestHandler):
parsed = url.urlsplit(self.path)
path = parsed.path
query = {k: v[0] for k, v in url.parse_qs(parsed.query).items()}
if path == baidu.CALLBACK:
try:
BAIDU.callback(query, self.headers.get('Cookie', '')); result = 'ok'
except Problem: result = 'failed'
self.send_response(303)
self.send_header('Location', '/?baidu='+result)
self.send_header('Cache-Control', 'no-store')
self.send_header('Referrer-Policy', 'no-referrer')
self.send_header('Set-Cookie', 'baidu_flow=; Path=/api/baidu/callback; HttpOnly; SameSite=Lax; Secure; Max-Age=0')
self.send_header('Content-Length', '0'); self.end_headers()
return
if path == '/api/branding':
self.answer(APPEARANCE.public())
return
@@ -862,6 +908,34 @@ class Handler(BaseHTTPRequestHandler):
self.require_auth()
if path == '/api/state':
self.answer(ACCOUNTS.state(self.user, status()))
elif path == '/api/baidu/config':
self.require_admin(); self.answer(BAIDU.public_config())
elif path == '/api/baidu/status':
self.answer(BAIDU.status(self.user))
elif path == '/api/journal':
self.answer(JOURNAL.snapshot(self.user, query))
elif path == '/media/journal':
self.user = HOUSEHOLDS.context(self.user, query.get('family'))
item = JOURNAL.media(self.user, query.get('id'))
with BAIDU.open_file('shared', item, self.headers.get('Range', '')) as response:
if response.status not in (200, 206): raise Problem('网盘媒体暂时不可用', 502)
self.send_response(response.status)
self.send_header('Content-Type', item['mime'])
self.send_header('Cache-Control', 'no-store')
self.send_header('X-Content-Type-Options', 'nosniff')
self.send_header('Referrer-Policy', 'no-referrer')
self.send_header('Accept-Ranges', 'bytes')
self.send_header('Content-Disposition', ('attachment' if query.get('download') == '1' else 'inline')+"; filename*=UTF-8''"+url.quote(item['name']))
for header in ('Content-Length', 'Content-Range'):
if response.headers.get(header): self.send_header(header, response.headers[header])
self.end_headers()
remaining = item['size']
try:
while remaining > 0:
chunk = response.read(min(65536, remaining))
if not chunk: break
self.wfile.write(chunk); remaining -= len(chunk)
except (OSError, http.client.HTTPException): self.close_connection = True
elif path == '/api/preferences':
self.answer(APPEARANCE.preferences(self.user))
elif path == '/api/appearance':
@@ -970,6 +1044,7 @@ class Handler(BaseHTTPRequestHandler):
'/family.js': 'family.js', '/kinship.js': 'kinship.js', '/households.js': 'households.js',
'/device-model.js': 'device-model.js', '/devices.js': 'devices.js',
'/steward.js': 'steward.js', '/heritage.js': 'heritage.js', '/personalization.js': 'personalization.js',
'/journal.js': 'journal.js', '/file-hash.js': 'file-hash.js',
'/family-graph.js': 'family-graph.js', '/family-map.js': 'family-map.js',
'/family-map.css': 'family-map.css', '/chinese-theme.css': 'chinese-theme.css',
'/family-events.js': 'family-events.js',
@@ -1009,6 +1084,7 @@ def initialize():
ACCOUNTS.initialize()
HOUSEHOLDS.initialize()
HERITAGE.initialize()
JOURNAL.initialize()
if not setting('storage'):
set_setting('storage', {'retentionDays': 7, 'maxGB': 40, 'reserveGB': 8})
if not setting('mediaSecret'):