Add support for content bundles

master
Dan Winship 17 years ago
parent 9858a190fd
commit 7b760686a7

@ -84,6 +84,7 @@ services/console/interface/logviewer/Makefile
services/console/interface/terminal/Makefile
sugar/Makefile
sugar/activity/Makefile
sugar/bundle/Makefile
sugar/clipboard/Makefile
sugar/graphics/Makefile
sugar/objects/Makefile

@ -4,4 +4,8 @@
<_comment>Sugar activity bundle</_comment>
<glob pattern="*.xo"/>
</mime-type>
<mime-type type="application/vnd.olpc-content">
<_comment>Sugar content bundle</_comment>
<glob pattern="*.xol"/>
</mime-type>
</mime-info>

@ -1,4 +1,4 @@
SUBDIRS = activity clipboard graphics objects presence datastore
SUBDIRS = activity bundle clipboard graphics objects presence datastore
sugardir = $(pythondir)/sugar
sugar_PYTHON = \

@ -29,6 +29,9 @@ import dbus
from sugar import env
from sugar import activity
from sugar.bundle.bundle import AlreadyInstalledException, \
NotInstalledException, InvalidPathException, ZipExtractException, \
RegistrationException, MalformedBundleException
_PYTHON_FACTORY='sugar-activity-factory'
@ -36,13 +39,6 @@ _DBUS_SHELL_SERVICE = "org.laptop.Shell"
_DBUS_SHELL_PATH = "/org/laptop/Shell"
_DBUS_ACTIVITY_REGISTRY_IFACE = "org.laptop.Shell.ActivityRegistry"
class AlreadyInstalledException(Exception): pass
class NotInstalledException(Exception): pass
class InvalidPathException(Exception): pass
class ZipExtractException(Exception): pass
class RegistrationException(Exception): pass
class MalformedBundleException(Exception): pass
class Bundle:
"""Metadata description of a given application/activity

@ -0,0 +1,5 @@
sugardir = $(pythondir)/sugar/bundle
sugar_PYTHON = \
__init__.py \
bundle.py \
contentbundle.py

@ -0,0 +1,16 @@
# Copyright (C) 2006-2007, Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

@ -0,0 +1,145 @@
# Copyright (C) 2007, Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
"""Sugar bundle file handler"""
import os
import StringIO
import zipfile
class AlreadyInstalledException(Exception): pass
class NotInstalledException(Exception): pass
class InvalidPathException(Exception): pass
class ZipExtractException(Exception): pass
class RegistrationException(Exception): pass
class MalformedBundleException(Exception): pass
class Bundle:
"""A Sugar activity, content module, etc.
The bundle itself may be either a zip file or a directory
hierarchy, with metadata about the bundle stored various files
inside it.
This is an abstract base class. See ActivityBundle and
ContentBundle for more details on those bundle types.
"""
def __init__(self, path):
self._path = path
if os.path.isdir(self._path):
self._unpacked = True
else:
self._unpacked = False
self._check_zip_bundle()
# manifest = self._get_file(self._infodir + '/contents')
# if manifest is None:
# raise MalformedBundleException('No manifest file')
#
# signature = self._get_file(self._infodir + '/contents.sig')
# if signature is None:
# raise MalformedBundleException('No signature file')
def _check_zip_bundle(self):
zip_file = zipfile.ZipFile(self._path)
file_names = zip_file.namelist()
if len(file_names) == 0:
raise MalformedBundleException('Empty zip file')
self._zip_root_dir = file_names[0].split('/')[0]
if self._unzipped_extension is not None:
(name, ext) = os.path.splitext(self._zip_root_dir)
if ext != self._unzipped_extension:
raise MalformedBundleException(
'All files in the bundle must be inside a single ' +
'directory whose name ends with "%s"' %
self._unzipped_extension)
for file_name in file_names:
if not file_name.startswith(self._zip_root_dir):
raise MalformedBundleException(
'All files in the bundle must be inside a single ' +
'top-level directory')
def _get_file(self, filename):
file = None
if self._unpacked:
path = os.path.join(self._path, filename)
if os.path.isfile(path):
file = open(path)
else:
zip_file = zipfile.ZipFile(self._path)
path = os.path.join(self._zip_root_dir, filename)
try:
data = zip_file.read(path)
file = StringIO.StringIO(data)
except KeyError:
# == "file not found"
pass
zip_file.close()
return file
def get_path(self):
"""Get the bundle path."""
return self._path
def _unzip(self, install_dir):
if self._unpacked:
raise AlreadyInstalledException
if not os.path.isdir(install_dir):
os.mkdir(install_dir)
# zipfile provides API that in theory would let us do this
# correctly by hand, but handling all the oddities of
# Windows/UNIX mappings, extension attributes, deprecated
# features, etc makes it impractical.
# FIXME: use manifest
if os.spawnlp(os.P_WAIT, 'unzip', 'unzip', self._path,
'-d', install_dir):
raise ZipExtractException
def _zip(self, bundle_path):
if not self._unpacked:
raise NotInstalledException
# FIXME: use manifest
zip = zipfile.ZipFile(bundle_path, 'w', zipfile.ZIP_DEFLATED)
for root, dirs, files in os.walk(self._path):
for name in files:
zip.write(filename, os.path.join(base_dir, filename))
zip.close()
def remove(self):
ext = os.path.splitext(self._path)[1]
if self._unpacked:
if not os.path.isdir(self._path) or ext != self._unzipped_extension:
raise InvalidPathException
for root, dirs, files in os.walk(self._path, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
os.rmdir(self._path)
else:
if not os.path.isfile(self._path) or ext != self._zipped_extension:
raise InvalidPathException
os.remove(self._path)

@ -0,0 +1,182 @@
# Copyright (C) 2007, Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
"""Sugar content bundles"""
from ConfigParser import ConfigParser
import os
from sugar import env
from sugar.bundle.bundle import Bundle
class ContentBundle(Bundle):
"""A Sugar content bundle
See http://wiki.laptop.org/go/Content_bundles for details
"""
MIME_TYPE = 'application/vnd.olpc-content'
_zipped_extension = '.xol'
_unzipped_extension = None
_infodir = 'library'
def __init__(self, path):
Bundle.__init__(self, path)
info_file = self._get_file('library/library.info')
if info_file is None:
raise MalformedBundleException('No library.info file')
self._parse_info(info_file)
if (self._get_file('index.html') is None and
self._get_file('library/library.xml') is None):
raise MalformedBundleException(
'Content bundle %s has neither index.html nor library.xml' %
self._path)
def _parse_info(self, info_file):
cp = ConfigParser()
cp.readfp(info_file)
section = 'Library'
if cp.has_option(section, 'host_version'):
version = cp.get(section, 'host_version')
try:
if int(version) != 1:
raise MalformedBundleException(
'Content bundle %s has unknown host_version number %s' %
(self._path, version))
except ValueError:
raise MalformedBundleException(
'Content bundle %s has invalid host_version number %s' %
(self._path, version))
if cp.has_option(section, 'name'):
self._name = cp.get(section, 'name')
else:
raise MalformedBundleException(
'Content bundle %s does not specify a name' % self._path)
if cp.has_option(section, 'library_version'):
version = cp.get(section, 'library_version')
try:
self._library_version = int(version)
except ValueError:
raise MalformedBundleException(
'Content bundle %s has invalid version number %s' %
(self._path, version))
if cp.has_option(section, 'l10n'):
l10n = cp.get(section, 'l10n')
if l10n == 'true':
self._l10n = True
elif l10n == 'false':
self._l10n = False
else:
raise MalformedBundleException(
'Content bundle %s has invalid l10n key "%s"' %
(self._path, l10n))
else:
raise MalformedBundleException(
'Content bundle %s does not specify if it is localized' %
self._path)
if cp.has_option(section, 'locale'):
self._locale = cp.get(section, 'locale')
else:
raise MalformedBundleException(
'Content bundle %s does not specify a locale' % self._path)
if cp.has_option(section, 'category'):
self._category = cp.get(section, 'category')
else:
raise MalformedBundleException(
'Content bundle %s does not specify a category' % self._path)
if cp.has_option(section, 'category_icon'):
self._category_icon = cp.get(section, 'category_icon')
else:
self._category_icon = None
if cp.has_option(section, 'category_class'):
self._category_class = cp.get(section, 'category_class')
else:
self._category_class = None
if cp.has_option(section, 'subcategory'):
self._subcategory = cp.get(section, 'subcategory')
else:
self._subcategory = None
if cp.has_option(section, 'bundle_class'):
self._bundle_class = cp.get(section, 'bundle_class')
else:
self._bundle_class = None
def get_name(self):
return self._name
def get_library_version(self):
return self._library_version
def get_l10n(self):
return self._l10n
def get_locale(self):
return self._locale
def get_category(self):
return self._category
def get_category(self):
return self._category
def get_category_icon(self):
return self._category_icon
def get_category_class(self):
return self._category_class
def get_subcategory(self):
return self._subcategory
def get_bundle_class(self):
return self._bundle_class
def _run_indexer(self):
os.spawnlp(os.P_WAIT, 'python',
'python',
os.path.join(env.get_user_library_path(), 'makeIndex.py'))
def is_installed(self):
if self._unpacked:
return True
elif os.path.isdir(os.path.join(env.get_user_library_path(),
self._zip_root_dir)):
return True
else:
return False
def install(self):
self._unzip(env.get_user_library_path())
self._run_indexer()
def uninstall(self):
self._uninstall()
self._run_indexer()

@ -26,6 +26,7 @@ from sugar import activity
from sugar.activity.bundle import Bundle
from sugar.activity import activityfactory
from sugar.activity.activityhandle import ActivityHandle
from sugar.bundle.contentbundle import ContentBundle
class DSMetadata(gobject.GObject):
__gsignals__ = {
@ -118,6 +119,10 @@ class DSObject(object):
return activities
def is_content_bundle(self):
return self.metadata['mime_type'] == ContentBundle.MIME_TYPE
# FIXME: should become is_activity_bundle()
def is_bundle(self):
return self.metadata['mime_type'] in ['application/vnd.olpc-x-sugar',
'application/vnd.olpc-sugar']

@ -67,6 +67,9 @@ def get_profile_path(path=None):
def get_user_activities_path():
return os.path.expanduser('~/Activities')
def get_user_library_path():
return os.path.expanduser('~/Library')
def get_locale_path(path=None):
return _get_prefix_path('share/locale', path)

Loading…
Cancel
Save