Can now "upload" a plugin (which actually just copies it to the right place)
This commit is contained in:
Родитель
41851edb87
Коммит
b924046c26
|
@ -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"
|
||||
|
Загрузка…
Ссылка в новой задаче