Can now "upload" a plugin (which actually just copies it to the right place)

This commit is contained in:
Kevin Dangoor 2010-03-29 21:37:08 -04:00
Родитель 41851edb87
Коммит b924046c26
8 изменённых файлов: 320 добавлений и 70 удалений

Просмотреть файл

@ -1182,6 +1182,22 @@ def reload_plugin(request, response):
plugin = plugins.lookup_plugin(plugin_name, path)
return _plugin_response(response, plugin_list=[plugin])
@expose(r'^/plugin/upload/(?P<plugin_name>.+)', 'POST')
def upload_plugin(request, response):
plugin_name = request.kwargs['plugin_name']
if ".." in plugin_name:
raise BadRequest("'..' not allowed in plugin names")
path = _get_user_plugin_path(request)
plugin = plugins.lookup_plugin(plugin_name, path)
if not plugin:
raise plugins.PluginError("Cannot find plugin %s" % plugin_name)
if plugin.location_name != "user":
raise BadRequest("Only user-editable plugins can be uploaded")
plugins.save_to_gallery(request.user, plugin.location)
response.content_type = "text/plain"
response.body = "OK"
return response()
def _wrap_script(plugin_name, script_path, script_text):
if script_path:

Просмотреть файл

@ -781,12 +781,26 @@ def log_event(kind, user, details=None):
conn = session.connection()
conn.execute(ins)
class Gallery(Base):
class GalleryPlugin(Base):
"""Plugin Gallery entries"""
__tablename__ = "gallery"
id = Column(Integer, primary_key=True)
owner_id=Column(Integer, ForeignKey('users.id', ondelete="cascade"))
name=Column(String(128))
name=Column(String(128), unique=True)
version=Column(String(30))
packageInfo=Column(PickleType())
def __init__(self, owner, name):
self.owner_id = owner.id
self.name = name
@classmethod
def get_plugin(cls, user, name):
s = _get_session()
plugin = s.query(cls).filter_by(name=name).first()
if not plugin:
plugin = cls(user, name)
s.add(plugin)
return plugin

Просмотреть файл

@ -35,7 +35,7 @@ class Gallery(Base):
id = Column(Integer, primary_key=True)
owner_id=Column(Integer, ForeignKey('users.id', ondelete="cascade"))
name=Column(String(128))
name=Column(String(128), unique=True)
version=Column(String(30))
packageInfo=Column(PickleType())

Просмотреть файл

@ -41,75 +41,16 @@ import time
from dryice.plugins import (Plugin as BasePlugin,
find_plugins as base_find_plugins,
lookup_plugin as base_lookup_plugin)
lookup_plugin as base_lookup_plugin,
get_metadata)
from bespin import config
from bespin import VERSION
from bespin.database import GalleryPlugin
class PluginError(Exception):
pass
_required_fields = set(["name", "description", "version", "licenses"])
_upper_case = re.compile("[A-Z]")
_beginning_letter = re.compile("^[a-zA-Z]")
_illegal_characters = re.compile(r"[^\w\._\-]")
_semver1 = re.compile(r"^\d+[A-Za-z0-9\-]*$")
_semver2 = re.compile(r"^\d+\.\d+[A-Za-z0-9\-]*$")
_semver3 = re.compile(r"^\d+\.\d+\.\d+[A-Za-z0-9\-]*$")
def _validate_metadata(metadata):
"""Ensures that plugin metadata is valid for inclusion in the
Gallery."""
errors = set([])
for field in _required_fields:
if field not in metadata:
errors.add("%s is required" % (field))
try:
name = metadata['name']
if _upper_case.search(name):
errors.add("name must be lower case")
if not _beginning_letter.search(name):
errors.add("name must begin with a letter")
if _illegal_characters.search(name):
errors.add("name may only contain letters, numbers, '.', '_' and '-'")
except KeyError:
pass
try:
version = metadata['version']
if not _semver1.match(version) and not _semver2.match(version) \
and not _semver3.match(version):
errors.add("version should be of the form X(.Y)(.Z)(alpha/beta/etc) http://semver.org")
except KeyError:
pass
try:
keywords = metadata['keywords']
if not isinstance(keywords, list):
errors.add("keywords should be an array of strings")
else:
for kw in keywords:
if not isinstance(kw, basestring):
errors.add("keywords should be an array of strings")
break
except KeyError:
pass
try:
licenses = metadata['licenses']
if not isinstance(licenses, list):
errors.add("licenses should be an array of objects http://semver.org")
else:
for l in licenses:
if not isinstance(l, dict):
errors.add("licenses should be an array of objects http://semver.org")
break
except KeyError:
pass
return errors
class Plugin(BasePlugin):
def load_metadata(self):
md = super(Plugin, self).load_metadata()
@ -210,6 +151,125 @@ def install_plugin(f, url, settings_project, path_entry, plugin_name=None):
plugin = Plugin(plugin_name, destination, path_entry)
return plugin
def saveToGallery(user, location):
pass
# Plugin Gallery functionality
_required_fields = set(["name", "description", "version", "licenses"])
_upper_case = re.compile("[A-Z]")
_beginning_letter = re.compile("^[a-zA-Z]")
_illegal_characters = re.compile(r"[^\w\._\-]")
_semver1 = re.compile(r"^\d+[A-Za-z0-9\-]*$")
_semver2 = re.compile(r"^\d+\.\d+[A-Za-z0-9\-]*$")
_semver3 = re.compile(r"^\d+\.\d+\.\d+[A-Za-z0-9\-]*$")
def _validate_version_string(version):
if not _semver1.match(version) and not _semver2.match(version) \
and not _semver3.match(version):
return False
return True
def _validate_metadata(metadata):
"""Ensures that plugin metadata is valid for inclusion in the
Gallery."""
errors = set([])
for field in _required_fields:
if field not in metadata:
errors.add("%s is required" % (field))
try:
name = metadata['name']
if _upper_case.search(name):
errors.add("name must be lower case")
if not _beginning_letter.search(name):
errors.add("name must begin with a letter")
if _illegal_characters.search(name):
errors.add("name may only contain letters, numbers, '.', '_' and '-'")
except KeyError:
pass
try:
version = metadata['version']
if not _validate_version_string(version):
errors.add("version should be of the form X(.Y)(.Z)(alpha/beta/etc) http://semver.org")
except KeyError:
pass
try:
keywords = metadata['keywords']
if not isinstance(keywords, list):
errors.add("keywords should be an array of strings")
else:
for kw in keywords:
if not isinstance(kw, basestring):
errors.add("keywords should be an array of strings")
break
except KeyError:
pass
try:
licenses = metadata['licenses']
if not isinstance(licenses, list):
errors.add("licenses should be an array of objects http://semver.org")
else:
for l in licenses:
if not isinstance(l, dict):
errors.add("licenses should be an array of objects http://semver.org")
break
except KeyError:
pass
if "depends" in metadata:
errors.add("'depends' is not longer supported. use dependencies.")
try:
dependencies = metadata['dependencies']
if not isinstance(dependencies, dict):
errors.add('dependencies should be a dictionary')
else:
for dependName, info in dependencies.items():
if isinstance(info, basestring):
if not _validate_version_string(info):
errors.add("'%s' is not a valid version for dependency '%s'"
% (info, dependName))
elif isinstance(info, list):
for v in info:
if not _validate_version_string(v):
errors.add("'%s' is not a valid version for dependency '%s'"
% (v, dependName))
else:
errors.add("'%s' is not a valid version for dependency '%s'"
% (info, dependName))
except KeyError:
pass
return errors
def save_to_gallery(user, location):
"""This is how a new plugin or new version of a plugin gets into the
gallery. Note that any errors will result in a PluginError exception
being raised."""
metadata, errors = get_metadata(location)
if errors:
raise PluginError("Errors found when reading plugin metadata: %s" % (errors,))
errors = _validate_metadata(metadata)
if errors:
raise PluginError("Errors in plugin metadata: %s" % (errors,))
plugin = GalleryPlugin.get_plugin(user, metadata['name'])
if not plugin.version:
plugin.version = metadata['version']
plugin.packageInfo = metadata
gallery_root = config.c.gallery_root
plugin_dir = gallery_root / metadata['name']
if not plugin_dir.exists():
plugin_dir.makedirs()
if location.isdir():
location.copytree(plugin_dir / metadata['version'])
else:
location.copy(plugin_dir / (metadata['version'] + ".js"))

Просмотреть файл

@ -0,0 +1,55 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Bespin.
*
* The Initial Developer of the Original Code is
* Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
"define metadata";
({
"name": "singlefileplugin3",
"description": "A single file plugin to install.",
"licenses": [
{
"type": "MPL"
}
],
"version": "2.3.2"
});
"end";
exports.powerfulCode = function(two) {
if (two !== 2) {
throw new Error("two is not 2");
}
return 2+2;
};

Просмотреть файл

@ -1,3 +1,13 @@
{
"depends": ["plugin2"]
"name": "plugin1",
"description": "plugin the first.",
"version": "0.9",
"licenses": [
{
"type": "MPL"
}
],
"dependencies": {
"plugin2": "1.0"
}
}

Просмотреть файл

@ -1,3 +1,10 @@
{
"name": "plugin2",
"description": "second plugin.",
"licenses": [
{
"type": "MPL"
}
],
"version": "1.0.1"
}

Просмотреть файл

@ -40,7 +40,7 @@ from path import path
from simplejson import loads
from bespin import config, plugins, controllers
from bespin.database import User, Base, EventLog, _get_session
from bespin.database import User, Base, EventLog, _get_session, GalleryPlugin
from bespin.filesystem import get_project
from __init__ import BespinTestApp
@ -311,4 +311,92 @@ def test_plugin_metadata_validation():
data['licenses'] = ["GPL"]
result = vm(data)
assert result == set(["licenses should be an array of objects http://semver.org"])
data = dict(good_metadata)
data['depends'] = ["foo"]
result = vm(data)
assert result == set(["'depends' is not longer supported. use dependencies."])
data = dict(good_metadata)
data['dependencies'] = ['foo']
result = vm(data)
assert result == set(['dependencies should be a dictionary'])
data['dependencies'] = dict(foo="bar")
result = vm(data)
assert result == set(["'bar' is not a valid version for dependency 'foo'"])
data['dependencies'] = dict(foo="1.0.0")
result = vm(data)
assert result == set()
data['depedencies'] = dict(foo=["1.0", "2.0"])
result = vm(data)
assert result == set()
data['dependencies'] = dict(foo = ["invalid"])
result = vm(data)
assert result == set(["'invalid' is not a valid version for dependency 'foo'"])
def test_save_plugin_without_enough_metadata():
try:
plugins.save_to_gallery(macgyver, plugindir / "SingleFilePlugin1.js")
assert False, "Expected to get an exception when saving a plugin without enough metadata"
except plugins.PluginError:
pass
def test_save_plugin_good():
_init_data()
gallery_root = config.c.gallery_root
plugins.save_to_gallery(macgyver, plugindir / "plugin1")
plugin1_dir = gallery_root / "plugin1"
assert plugin1_dir.exists()
version_dir = plugin1_dir / "0.9"
assert version_dir.exists()
assert version_dir.isdir()
package_info = version_dir / "package.json"
assert package_info.exists()
s = config.c.session_factory()
s.commit()
s.clear()
num_plugins = s.query(GalleryPlugin).count()
assert num_plugins == 1
plugin = s.query(GalleryPlugin).first()
assert plugin.name == "plugin1"
assert plugin.version == "0.9"
assert plugin.packageInfo['description'] == "plugin the first."
def test_save_single_file_plugin_to_gallery():
_init_data()
gallery_root = config.c.gallery_root
plugins.save_to_gallery(macgyver, plugindir / "SingleFilePlugin3.js")
sfp3_dir = gallery_root / "singlefileplugin3"
assert sfp3_dir.exists()
version_file = sfp3_dir / "2.3.2.js"
assert version_file.exists()
assert not version_file.isdir()
def test_plugin_upload_from_the_web():
_init_data()
_init_data()
sfp = (path(__file__).dirname() / "plugindir").abspath() / "SingleFilePlugin3.js"
sfp_content = sfp.text()
response = app.put("/file/at/myplugins/singlefileplugin3.js", sfp_content)
response = app.put("/file/at/BespinSettings/pluginInfo.json", """{
"plugins": ["myplugins/singlefileplugin3.js"],
"pluginOrdering": ["singlefileplugin3"]
}""")
response = app.post("/plugin/upload/singlefileplugin3")
assert response.body == "OK"
s = config.c.session_factory()
num_plugins = s.query(GalleryPlugin).count()
assert num_plugins == 1
plugin = s.query(GalleryPlugin).first()
assert plugin.name == "singlefileplugin3"