diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2014-04-29 07:53:52 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2014-04-29 16:40:28 (GMT) |
commit | 7b650854958d03386ae54e87cbbb2bc82053661e (patch) | |
tree | 486f54e50d58e21aedda86fcbc19237d921f401a /misc/aslo-sync | |
parent | fe07c5f9f1da79059f8fcd5e33e0f043b7cc6814 (diff) |
Fix aslo-sync script
Diffstat (limited to 'misc/aslo-sync')
-rwxr-xr-x | misc/aslo-sync | 433 |
1 files changed, 215 insertions, 218 deletions
diff --git a/misc/aslo-sync b/misc/aslo-sync index 147ac04..141883e 100755 --- a/misc/aslo-sync +++ b/misc/aslo-sync @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright (C) 2012-2013 Aleksey Lim +# Copyright (C) 2012-2014 Aleksey Lim # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,16 +21,20 @@ import time import getpass import hashlib import traceback -from cStringIO import StringIO from os.path import join import MySQLdb as mdb -from sugar_network import db, client, model, toolkit -from sugar_network.node import data_root +from sugar_network import db, toolkit +from sugar_network.node import data_root, master_api +from sugar_network.node.auth import Principal +from sugar_network.node.master import RESOURCES from sugar_network.node.slave import SlaveRoutes -from sugar_network.node.routes import load_bundle -from sugar_network.toolkit import spec, licenses, application, Option +from sugar_network.node.model import load_bundle +from sugar_network.toolkit.spec import parse_version +from sugar_network.toolkit.router import Request, Router +from sugar_network.toolkit.coroutine import this +from sugar_network.toolkit import licenses, application, Option DOWNLOAD_URL = 'http://download.sugarlabs.org/activities' @@ -42,6 +46,15 @@ SUGAR_GUID = 'sugar' SN_GUID = 'sugar-network' PACKAGES_GUID = 'packages' +SUGAR_API_COMPATIBILITY = { + '0.94': [ + parse_version('0.86'), + parse_version('0.88'), + parse_version('0.90'), + parse_version('0.92'), + ], + } + CATEGIORIES_TO_TAGS = { 'Search & Discovery': 'discovery', 'Documents': 'productivity', @@ -94,6 +107,12 @@ IGNORE_VERSIONS = frozenset([ 30703, # Bad license 31164, # Bad bundle_id 31512, # Bad license + 30749, # Changed bundle_id + 31238, # Changed bundle_id + 31418, # Changed bundle_id + 31369, # Malformed version + 31557, # Malformed version + 31454, # Malformed version ]) IGNORE_PREVIEWS = frozenset([ @@ -138,19 +157,22 @@ LICENSES_MAP = { class Application(application.Application): _my_connection = None - _volume = None _client = None - - @property - def volume(self): - if self._volume is None: - self._volume = db.Volume(data_root.value, model.RESOURCES) - self._volume.populate() - return self._volume + _router = None + + def prolog(self): + this.volume = db.Volume(data_root.value, RESOURCES) + this.volume.populate() + this.broadcast = lambda event: None + this.localcast = lambda event: None + this.request = Request({'HTTP_HOST': master_api.value}) + auth = _Auth() + routes = SlaveRoutes(master_api.value, this.volume, auth=auth) + self._router = Router(routes) + this.principal = auth.logon() def epilog(self): - if self._volume is not None: - self._volume.close() + this.volume.close() @application.command( 'consecutively launch pull and push commands') @@ -161,27 +183,25 @@ class Application(application.Application): @application.command( 'pull activities.sugarlabs.org content to local db') def pull(self): - if not self.volume['context'].exists(SN_GUID): - self.volume['context'].create({ + if not this.volume['context'][SN_GUID].exists: + this.volume['context'].create({ 'guid': SN_GUID, - 'implement': SN_GUID, - 'type': 'project', - 'title': 'Sugar Network', - 'summary': 'Sugar Network', - 'description': 'Sugar Network', - 'ctime': time.time(), - 'mtime': time.time(), + 'type': ['group'], + 'title': {'en': 'Sugar Network'}, + 'summary': {'en': 'Sugar Network'}, + 'description': {'en': 'Sugar Network'}, + 'ctime': int(time.time()), + 'mtime': int(time.time()), 'author': ASLO_AUTHOR, }) - if not self.volume['context'].exists(SUGAR_GUID): - self.volume['context'].create({ + if not this.volume['context'][SUGAR_GUID].exists: + this.volume['context'].create({ 'guid': SUGAR_GUID, - 'implement': SUGAR_GUID, - 'type': 'package', - 'title': 'sugar', - 'summary': 'Constructionist learning platform', - 'description': + 'type': ['package'], + 'title': {'en': 'sugar'}, + 'summary': {'en': 'Constructionist learning platform'}, + 'description': {'en': 'Sugar provides simple yet powerful means of engaging ' 'young children in the world of learning that is ' 'opened up by computers and the Internet. With Sugar, ' @@ -190,26 +210,28 @@ class Application(application.Application): 'in authentic problem-solving. Sugar promotes ' 'sharing, collaborative learning, and reflection, ' 'developing skills that help them in all aspects ' - 'of life.', - 'ctime': time.time(), - 'mtime': time.time(), + 'of life.'}, + 'ctime': int(time.time()), + 'mtime': int(time.time()), 'author': ASLO_AUTHOR, }) - if not self.volume['context'].exists(PACKAGES_GUID): - self.volume['context'].create({ + if not this.volume['context'][PACKAGES_GUID].exists: + this.volume['context'].create({ 'guid': PACKAGES_GUID, - 'implement': PACKAGES_GUID, - 'type': 'project', - 'title': 'Packages', - 'summary': 'Collection of GNU/Linux packages metadata', - 'description': 'Collection of GNU/Linux packages metadata', - 'ctime': time.time(), - 'mtime': time.time(), - 'author': ASLO_AUTHOR, - 'icon': { - 'url': '/static/images/package.png', + 'type': ['group'], + 'title': {'en': 'Packages'}, + 'summary': { + 'en': 'Collection of GNU/Linux packages metadata', }, + 'description': { + 'en': 'Collection of GNU/Linux packages metadata', + }, + 'ctime': int(time.time()), + 'mtime': int(time.time()), + 'author': ASLO_AUTHOR, + 'icon': 'assets/package.png', + 'logo': 'assets/package-logo.png', }) if self.args: @@ -222,13 +244,12 @@ class Application(application.Application): 'submit pulled activities.sugarlabs.org content to ' 'Sugar Network server') def push(self): - node = SlaveRoutes(join(data_root.value, 'key'), self.volume) - node.online_sync(no_pull=True) + this.call(method='POST', cmd='online_sync', no_pull=True) def sync_activities(self, addon_id=None): - directory = self.volume['context'] + directory = this.volume['context'] items, __ = directory.find(type='activity', guid=addon_id, - not_layer='deleted') + not_state='deleted') existing_activities = set([i.guid for i in items]) sql = """ @@ -251,7 +272,6 @@ class Application(application.Application): self.sync_versions(addon_id, bundle_id) self.sync_reviews(addon_id, bundle_id) self.sync_previews(addon_id, bundle_id, authors) - self.sync_comments(addon_id, bundle_id) except Exception: print '-- Failed to sync %s addon' % addon_id traceback.print_exception(*sys.exc_info()) @@ -260,116 +280,52 @@ class Application(application.Application): for guid in existing_activities: print '-- Hide %r deleted activity' % guid - directory.update(guid, {'layer': ['deleted']}) + directory.update(guid, {'state': 'deleted'}) def sync_previews(self, addon_id, bundle_id, authors): - directory = self.volume['artifact'] - items, __ = directory.find(context=bundle_id, type='preview', - not_layer='deleted') - existing = set([i.guid for i in items]) + existing = this.volume['context'][bundle_id]['previews'] + updates = {} sql = """ SELECT id, - created, modified, - caption, filedata FROM previews WHERE addon_id = %s """ % addon_id - for guid, created, modified, caption, data in self.sqlexec(sql): + for guid, modified, data in self.sqlexec(sql): if guid in IGNORE_PREVIEWS: continue guid = str(guid) - if directory.exists(guid): - existing.remove(guid) + if guid in existing: + del existing[guid] continue try: preview = scale_png(data, 200, 200) except Exception: print '-- Failed to load %s preview for %s' % (guid, bundle_id) continue - directory.create({ - 'guid': guid, - 'ctime': int(time.mktime(created.timetuple())), - 'mtime': int(time.mktime(modified.timetuple())), - 'context': bundle_id, - 'type': 'preview', - 'title': self.get_i18n_field(caption), - 'description': self.get_i18n_field(caption), + preview = this.volume.blobs.post(preview, 'image/png') + updates[guid] = { 'author': authors, - 'preview': { - 'blob': StringIO(preview), - 'mime_type': 'image/png', - 'digest': hashlib.sha1(preview).hexdigest(), - }, - 'data': { - 'blob': StringIO(data), - 'mime_type': 'image/png', - 'digest': hashlib.sha1(data).hexdigest(), - }, - }) + 'value': preview.digest, + 'ctime': int(time.mktime(modified.timetuple())), + } for guid in existing: print '-- Hide %s %s deleted preview' % (bundle_id, guid) - directory.update(guid, {'layer': ['deleted']}) - - def sync_comments(self, addon_id, bundle_id): - directory = self.volume['comment'] - items, __ = directory.find(context=bundle_id, not_layer='deleted') - existing = set([i.guid for i in items]) - - sql = """ - SELECT - reviews.id, - reviews.created, - reviews.modified, - reviews.body, - users.email, - users.nickname, - CONCAT_WS(' ', users.firstname, users.lastname), - reviews.reply_to - FROM - reviews - INNER JOIN versions ON versions.id = reviews.version_id - INNER JOIN users ON users.id=reviews.user_id - WHERE - reply_to IS NOT NULL AND versions.addon_id = %s - """ % addon_id - for guid, created, modified, content, email, nickname, fullname, \ - reply_to in self.sqlexec(sql): - guid = str(guid) - if directory.exists(guid): - existing.remove(guid) - continue - if not nickname: - nickname = email.split('@')[0] - fullname = fullname.strip() - if not fullname: - fullname = nickname - directory.create({ - 'guid': guid, - 'ctime': int(time.mktime(created.timetuple())), - 'mtime': int(time.mktime(modified.timetuple())), - 'context': bundle_id, - 'review': str(reply_to), - 'message': self.get_i18n_field(content), - 'author': {nickname: { - 'order': 0, 'role': 3, 'name': fullname, - }}, - }) + updates[guid] = {} - for guid in existing: - print '-- Hide %s %s deleted comment' % (bundle_id, guid) - directory.update(guid, {'layer': ['deleted']}) + this.volume['context'].update(bundle_id, {'previews': updates}) def sync_reviews(self, addon_id, bundle_id): - directory = self.volume['review'] - items, __ = directory.find(context=bundle_id, not_layer='deleted') - existing = set([i.guid for i in items]) + directory = this.volume['post'] + items, __ = directory.find(context=bundle_id, type='review', + not_state='deleted') + existing_topics = set([i.guid for i in items]) sql = """ SELECT @@ -389,38 +345,81 @@ class Application(application.Application): WHERE reply_to IS NULL AND versions.addon_id = %s """ % addon_id - for guid, created, modified, title, content, rating, email, nickname, \ + for topic, created, modified, title, content, vote, email, nickname, \ fullname in self.sqlexec(sql): - guid = str(guid) - if directory.exists(guid): - existing.remove(guid) - continue - if not nickname: - nickname = email.split('@')[0] - fullname = fullname.strip() - if not fullname: - fullname = nickname - directory.create({ - 'guid': guid, - 'ctime': int(time.mktime(created.timetuple())), - 'mtime': int(time.mktime(modified.timetuple())), - 'context': bundle_id, - 'title': self.get_i18n_field(title), - 'content': self.get_i18n_field(content), - 'rating': rating, - 'author': {nickname: { - 'order': 0, 'role': 3, 'name': fullname, - }}, - }) - - for guid in existing: + topic = str(topic) + if topic in existing_topics: + existing_topics.remove(topic) + else: + if not nickname: + nickname = email.split('@')[0] + fullname = fullname.strip() + if not fullname: + fullname = nickname + directory.create({ + 'guid': topic, + 'ctime': int(time.mktime(created.timetuple())), + 'mtime': int(time.mktime(modified.timetuple())), + 'context': bundle_id, + 'type': 'review', + 'title': self.get_i18n_field(title), + 'message': self.get_i18n_field(content), + 'vote': vote, + 'author': {nickname: { + 'order': 0, 'role': 3, 'name': fullname, + }}, + }) + + existing_comments = directory[topic]['comments'] + updates = {} + sql = """ + SELECT + reviews.id, + reviews.modified, + reviews.body, + users.email, + users.nickname, + CONCAT_WS(' ', users.firstname, users.lastname) + FROM + reviews + INNER JOIN versions ON versions.id = reviews.version_id + INNER JOIN users ON users.id=reviews.user_id + WHERE + reply_to = %s + ORDER BY + reviews.created + """ % topic + for guid, modified, content, email, nickname, fullname, \ + in self.sqlexec(sql): + guid = str(guid) + if guid in existing_comments: + del existing_comments[guid] + continue + if not nickname: + nickname = email.split('@')[0] + fullname = fullname.strip() + if not fullname: + fullname = nickname + updates[guid] = { + 'author': {nickname: { + 'order': 0, 'role': 3, 'name': fullname, + }}, + 'value': self.get_i18n_field(content), + 'ctime': int(time.mktime(modified.timetuple())), + } + for guid in existing_comments: + print '-- Hide %s %s deleted comment' % (bundle_id, guid) + updates[guid] = {} + directory.update(topic, {'comments': updates}) + + for guid in existing_topics: print '-- Hide %s %s deleted review' % (bundle_id, guid) - directory.update(guid, {'layer': ['deleted']}) + directory.update(guid, {'state': 'deleted'}) def sync_versions(self, addon_id, bundle_id): - directory = self.volume['release'] - items, __ = directory.find(context=bundle_id, not_layer='deleted') - existing = set([i.guid for i in items]) + existing = this.volume['context'][bundle_id]['releases'] + updates = {} + most_recent = True sql = """ SELECT @@ -438,8 +437,7 @@ class Application(application.Application): id=applications_versions.max), users.email, users.nickname, - CONCAT_WS(' ', users.firstname, users.lastname), - addons.status + CONCAT_WS(' ', users.firstname, users.lastname) FROM addons INNER JOIN versions ON versions.addon_id=addons.id LEFT JOIN licenses ON licenses.id=versions.license_id @@ -450,15 +448,19 @@ class Application(application.Application): WHERE addons.status > 0 AND addons.status < 5 AND addons.id = %s ORDER BY - versions.id ASC + versions.id DESC """ % addon_id for version_id, version, license_id, alicense, release_date, \ releasenotes, filename, sugar_min, sugar_max, \ - email, nickname, fullname, status in self.sqlexec(sql): + email, nickname, fullname in self.sqlexec(sql): if version_id in IGNORE_VERSIONS: continue + version_id = str(version_id) + if version_id in existing: + del existing[version_id] + continue if filename.endswith('.xol'): print '-- Ignore %r[%s] library bundle' % \ @@ -466,7 +468,7 @@ class Application(application.Application): continue try: - spec.parse_version(version) + parse_version(version) except Exception, error: print '-- Cannot parse %r version for %r[%s]: %s' % \ (version, filename, version_id, error) @@ -501,46 +503,49 @@ class Application(application.Application): if not alicense and bundle_id in LICENSES_MAP: alicense = LICENSES_MAP[bundle_id] - layers = ['origin'] - if status == 4: - layers.append('public') - if not nickname: nickname = email.split('@')[0] fullname = fullname.strip() if not fullname: fullname = nickname - if directory.exists(version_id): - if set(directory.get(version_id).layer) != set(layers) or \ - version_id not in existing: - directory.update(version_id, {'layer': layers}) - if version_id in existing: - existing.remove(version_id) - continue + for max_version, sub_versions in SUGAR_API_COMPATIBILITY.items(): + if parse_version(sugar_min) in sub_versions: + if parse_version(sugar_max) < parse_version(max_version): + sugar_max = max_version + elif parse_version(sugar_max) in sub_versions: + sugar_max = max_version bundle_path = join(ACTIVITIES_PATH, str(addon_id), filename) + digest = hashlib.sha1() + with file(bundle_path, 'rb') as f: + while True: + chunk = f.read(toolkit.BUFFER_SIZE) + if not chunk: + break + digest.update(chunk) + blob = this.volume.blobs.post({ + 'digest': digest.hexdigest(), + 'location': '/'.join([DOWNLOAD_URL, str(addon_id), filename]), + 'content-length': str(os.stat(bundle_path).st_size), + }) + blob.path = bundle_path + try: - with load_bundle( - self.volume, Request(self.volume, { - 'requires': 'sugar>=%s<=%s' % - (sugar_min, sugar_max), - 'license': alicense, - }), - file(bundle_path, 'rb')) as (impl, data): - impl['guid'] = version_id - if 'notes' not in impl: - impl['notes'] = self.get_i18n_field(releasenotes) - impl['stability'] = 'stable' - impl['ctime'] = int(time.mktime(release_date.timetuple())) - impl['mtime'] = time.time() - impl['author'] = {nickname: { - 'order': 0, 'role': 3, 'name': fullname, - }} - impl['layer'] = layers - data['url'] = \ - '/'.join([DOWNLOAD_URL, str(addon_id), filename]) - data['size'] = os.stat(bundle_path).st_size + __, release = load_bundle(blob, license=alicense, + extra_deps='sugar>=%s<=%s' % (sugar_min, sugar_max), + release_notes=self.get_i18n_field(releasenotes), + update_context=most_recent) + updates[version_id] = { + 'author': { + nickname: { + 'order': 0, 'role': 3, 'name': fullname, + }, + }, + 'value': release, + 'ctime': int(time.mktime(release_date.timetuple())), + } + most_recent = False except Exception, error: print '-- Failed to sync %r[%s]' % (filename, version_id) traceback.print_exception(*sys.exc_info()) @@ -549,10 +554,12 @@ class Application(application.Application): for guid in existing: print '-- Hide %s %s deleted version' % (bundle_id, guid) - directory.update(guid, {'layer': ['deleted']}) + updates[guid] = {} + + this.volume['context'].update(bundle_id, {'releases': updates}) def sync_context(self, addon_id, bundle_id): - directory = self.volume['context'] + directory = this.volume['context'] created, modified, title, summary, description, homepage, \ featured = self.sqlexec(""" @@ -573,11 +580,11 @@ class Application(application.Application): """ % addon_id)[0] created = int(time.mktime(created.timetuple())) modified = int(time.mktime(modified.timetuple())) - layers = ['featured'] if featured else [] + pins = ['featured'] if featured else [] - if directory.exists(bundle_id) and \ + if directory[bundle_id].exists and \ directory.get(bundle_id)['mtime'] >= modified and \ - directory.get(bundle_id)['layer'] == layers: + directory.get(bundle_id)['pins'] == pins: return tags = set() @@ -633,7 +640,7 @@ class Application(application.Application): directory.update(bundle_id, { 'guid': bundle_id, - 'type': 'activity', + 'type': ['activity'], 'title': self.get_i18n_field(title), 'summary': self.get_i18n_field(summary), 'description': self.get_i18n_field(description), @@ -642,7 +649,7 @@ class Application(application.Application): 'author': authors, 'ctime': created, 'mtime': modified, - 'layer': layers, + 'pins': pins, }) print '-- Sync %r activity' % bundle_id @@ -695,19 +702,10 @@ class Application(application.Application): return cursor.fetchall() -class Request(dict): - - def __init__(self, volume, props): - dict.__init__(self, props) - self._volume = volume +class _Auth(object): - def call(self, method, path, content): - if method == 'POST': - resource, = path - return self._volume[resource].create(content) - elif method == 'PUT': - resource, guid = path - self._volume[resource].update(guid, content) + def logon(self, request=None): + return Principal(ASLO_AUTHOR.keys()[0], 0xF) def scale_png(data, w, h): @@ -741,8 +739,7 @@ mysql_password = Option( Option.seek('main', [application.debug, toolkit.cachedir]) Option.seek('aslo', [mysql_server, mysql_user, mysql_password, mysql_database]) -Option.seek('node', [data_root]) -Option.seek('client', [client.api_url]) +Option.seek('node', [data_root, master_api]) db.index_write_queue.value = 1024 * 10 db.index_flush_threshold.value = 0 |