openmedialibrary/oml/changelog.py

299 lines
10 KiB
Python

# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
from __future__ import division
import logging
import json
from datetime import datetime
from utils import valid, datetime2ts, ts2datetime
import settings
import db
import sqlalchemy as sa
import state
from websocket import trigger_event
logger = logging.getLogger('oml.changelog')
class Changelog(db.Model):
'''
additem itemid metadata from file (info) + OLID
edititem itemid name->id (i.e. olid-> OL...M)
removeitem itemid
addlist name
editlist name {name: newname}
orderlists [name, name, name]
removelist name
addlistitems listname [ids]
removelistitems listname [ids]
editusername username
editcontact string
addpeer peerid peername
removepeer peerid peername
editmeta key, value data (i.e. 'isbn', '0000000000', {title: 'Example'})
resetmeta key, value
'''
__tablename__ = 'changelog'
id = sa.Column(sa.Integer(), primary_key=True)
created = sa.Column(sa.DateTime())
timestamp = sa.Column(sa.BigInteger())
user_id = sa.Column(sa.String(43))
revision = sa.Column(sa.BigInteger())
data = sa.Column(sa.Text())
sig = sa.Column(sa.String(96))
@classmethod
def record(cls, user, action, *args):
c = cls()
c.created = datetime.utcnow()
c.timestamp = datetime2ts(c.created)
c.user_id = user.id
c.revision = cls.query.filter_by(user_id=user.id).count()
c.data = json.dumps([action] + list(args))
_data = str(c.revision) + str(c.timestamp) + c.data
c.sig = settings.sk.sign(_data, encoding='base64')
db.session.add(c)
db.session.commit()
if state.nodes:
state.nodes.queue('peered', 'pushChanges', [c.json()])
@classmethod
def apply_changes(cls, user, changes):
for change in changes:
if not Changelog.apply_change(user, change, trigger=False):
logger.debug('FAIL %s', change)
break
return False
if changes:
trigger_event('change', {});
return True
@classmethod
def apply_change(cls, user, change, rebuild=False, trigger=True):
revision, timestamp, sig, data = change
last = Changelog.query.filter_by(user_id=user.id).order_by('-revision').first()
next_revision = last.revision + 1 if last else 0
if revision == next_revision:
_data = str(revision) + str(timestamp) + data
if rebuild:
sig = settings.sk.sign(_data, encoding='base64')
if valid(user.id, _data, sig):
c = cls()
c.created = datetime.utcnow()
c.timestamp = timestamp
c.user_id = user.id
c.revision = revision
c.data = data
c.sig = sig
args = json.loads(data)
logger.debug('apply change from %s: %s', user.name, args)
if getattr(c, 'action_' + args[0])(user, timestamp, *args[1:]):
logger.debug('change applied')
db.session.add(c)
db.session.commit()
if trigger:
trigger_event('change', {});
return True
else:
logger.debug('INVLAID SIGNATURE ON CHANGE %s', change)
raise Exception, 'invalid signature'
else:
logger.debug('revsion does not match! got %s expecting %s', revision, next_revision)
return False
def __repr__(self):
return self.data
def verify(self):
_data = str(self.revision) + str(self.timestamp) + self.data
return valid(self.user_id, _data, self.sig)
@classmethod
def _rebuild(cls):
for c in cls.query.filter_by(user_id=settings.USER_ID):
_data = str(c.revision) + str(c.timestamp) + c.data
c.sig = settings.sk.sign(_data, encoding='base64')
db.session.add(c)
db.session.commit()
def json(self):
timestamp = self.timestamp or datetime2ts(self.created)
return [self.revision, timestamp, self.sig, self.data]
@classmethod
def restore(cls, user_id, path=None):
from user.models import User
user = User.get_or_create(user_id)
if not path:
path = '/tmp/oml_changelog_%s.json' % user_id
with open(path, 'r') as fd:
for change in fd:
change = json.loads(change)
cls.apply_change(user, change, user_id == settings.USER_ID)
@classmethod
def export(cls, user_id, path=None):
if not path:
path = '/tmp/oml_changelog_%s.json' % user_id
with open(path, 'w') as fd:
for c in cls.query.filter_by(user_id=user_id).order_by('revision'):
fd.write(json.dumps(c.json()) + '\n')
def action_additem(self, user, timestamp, itemid, info):
from item.models import Item
i = Item.get(itemid)
if i and i.timestamp > timestamp:
if user not in i.users:
i.users.append(user)
i.update()
return True
if not i:
i = Item.get_or_create(itemid, info)
i.modified = ts2datetime(timestamp)
if user not in i.users:
i.users.append(user)
i.update()
return True
def action_edititem(self, user, timestamp, itemid, meta):
from item.models import Item
i = Item.get(itemid)
if not i:
logger.debug('ignore edititem for unknown item %s %s', timestamp, itemid)
return True
if i.timestamp > timestamp:
logger.debug('ignore edititem change %s %s %s', timestamp, itemid, meta)
return True
keys = filter(lambda k: k in Item.id_keys, meta.keys())
if keys:
key = keys[0]
primary = [key, meta[key]]
if not meta[key] and i.meta.get('primaryid', [''])[0] == key:
logger.debug('remove id mapping %s %s', i.id, primary)
i.update_primaryid(*primary)
i.modified = ts2datetime(timestamp)
elif meta[key] and i.meta.get('primaryid') != primary:
logger.debug('edit mapping %s %s', i.id, primary)
i.update_primaryid(*primary)
i.modified = ts2datetime(timestamp)
else:
if 'primaryid' in i.meta:
return True
i.update_meta(meta)
i.modified = ts2datetime(timestamp)
i.save()
return True
def action_removeitem(self, user, timestamp, itemid):
from item.models import Item
i = Item.get(itemid)
if not i or i.timestamp > timestamp:
return True
if user in i.users:
i.users.remove(user)
if i.users:
i.update()
else:
i.delete()
return True
def action_addlist(self, user, timestamp, name, query=None):
from user.models import List
l = List.create(user.id, name)
return True
def action_editlist(self, user, timestamp, name, new):
from user.models import List
l = List.get_or_create(user.id, name)
if 'name' in new:
l.name = new['name']
l.save()
return True
def action_orderlists(self, user, timestamp, lists):
from user.models import List
idx = 0
for name in lists:
l = List.get_or_create(user.id, name)
l.index_ = idx
l.save()
idx += 1
return True
def action_removelist(self, user, timestamp, name):
from user.models import List
l = List.get(user.id, name)
if l:
l.remove()
return True
def action_addlistitems(self, user, timestamp, name, ids):
from user.models import List
l = List.get_or_create(user.id, name)
l.add_items(ids)
return True
def action_removelistitems(self, user, timestamp, name, ids):
from user.models import List
l = List.get(user.id, name)
if l:
l.remove_items(ids)
return True
def action_editusername(self, user, timestamp, username):
from user.models import List
old = user.nickname
user.info['username'] = username
user.update_name()
if old != user.nickname:
List.rename_user(old, user.nickname)
user.save()
return True
def action_editcontact(self, user, timestamp, contact):
user.info['contact'] = contact
user.save()
return True
def action_addpeer(self, user, timestamp, peerid, username):
from user.models import User
if not 'users' in user.info:
user.info['users'] = {}
user.info['users'][peerid] = username
user.save()
peer = User.get_or_create(peerid)
if not 'username' in peer.info:
peer.info['username'] = username
peer.update_name()
peer.save()
return True
def action_removepeer(self, user, timestamp, peerid):
if 'users' in user.info and peerid in user.info['users']:
del user.info['users'][peerid]
user.save()
#fixme, remove from User table if no other connection exists
return True
def action_editmeta(self, user, timestamp, key, value, data):
from item.models import Metadata
m = Metadata.get(key, value)
if not m or m.timestamp < timestamp:
if not m:
m = Metadata.get_or_create(key, value)
if m.edit(data):
m.update_items()
return True
def action_resetmeta(self, user, timestamp, key, value):
from item.models import Metadata
m = Metadata.get(key, value)
if m and m.timestamp < timestamp:
m.reset()
return True