- { name: 'git.sshBackend', defaultValue: 'NIO2' }
- { name: 'git.sshCommandStartThreads', defaultValue: '2' }
- { name: 'plugins.folder', defaultValue: '${baseFolder}/plugins' }
+ - { name: 'plugins.registry', defaultValue: 'http://gitblit.github.io/gitblit-registry/plugins.json' }
}
#
# SINCE 1.4.0\r
tickets.perPage = 25\r
\r
+# The folder where plugins are loaded from.\r
+#\r
+# SINCE 1.5.0\r
+# RESTART REQUIRED\r
+# BASEFOLDER\r
+plugins.folder = ${baseFolder}/plugins\r
+\r
+# The registry of available plugins.\r
+#\r
+# SINCE 1.5.0\r
+plugins.registry = http://gitblit.github.io/gitblit-registry/plugins.json\r
+\r
#\r
# Groovy Integration\r
#\r
# SINCE 0.5.0\r
# RESTART REQUIRED\r
server.shutdownPort = 8081\r
-\r
-# Base folder for plugins.\r
-# This folder may contain Gitblit plugins\r
-#\r
-# SINCE 1.6.0\r
-# RESTART REQUIRED\r
-# BASEFOLDER\r
-plugins.folder = ${baseFolder}/plugins\r
import com.gitblit.models.GitClientApplication;
import com.gitblit.models.Mailing;
import com.gitblit.models.Metric;
+import com.gitblit.models.PluginRegistry.PluginRegistration;
+import com.gitblit.models.PluginRegistry.PluginRelease;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.RepositoryModel;
return repositoryManager.isIdle(repository);
}
+ /*
+ * PLUGIN MANAGER
+ */
+
@Override
public <T> List<T> getExtensions(Class<T> clazz) {
return pluginManager.getExtensions(clazz);
return pluginManager.deletePlugin(wrapper);
}
+ @Override
+ public boolean refreshRegistry() {
+ return pluginManager.refreshRegistry();
+ }
+
+ @Override
+ public boolean installPlugin(String url) {
+ return pluginManager.installPlugin(url);
+ }
+
+ @Override
+ public boolean installPlugin(PluginRelease pv) {
+ return pluginManager.installPlugin(pv);
+ }
+
+ @Override
+ public List<PluginRegistration> getRegisteredPlugins() {
+ return pluginManager.getRegisteredPlugins();
+ }
+
+ @Override
+ public PluginRegistration lookupPlugin(String idOrName) {
+ return pluginManager.lookupPlugin(idOrName);
+ }
+
+ @Override
+ public PluginRelease lookupRelease(String idOrName, String version) {
+ return pluginManager.lookupRelease(idOrName, version);
+ }
+
@Override
public List<PluginWrapper> getPlugins() {
return pluginManager.getPlugins();
*/
package com.gitblit.manager;
+import java.util.List;
+
import ro.fortsoft.pf4j.PluginManager;
import ro.fortsoft.pf4j.PluginWrapper;
+import com.gitblit.models.PluginRegistry.PluginRegistration;
+import com.gitblit.models.PluginRegistry.PluginRelease;
+
public interface IPluginManager extends IManager, PluginManager {
/**
* @return PluginWrapper that loaded the given class
*/
PluginWrapper whichPlugin(Class<?> clazz);
-
+
/**
* Delete the plugin represented by {@link PluginWrapper}.
- *
+ *
* @param wrapper
* @return true if successful
*/
boolean deletePlugin(PluginWrapper wrapper);
+
+ /**
+ * Refresh the plugin registry.
+ */
+ boolean refreshRegistry();
+
+ /**
+ * Install the plugin from the specified url.
+ */
+ boolean installPlugin(String url);
+
+ /**
+ * Install the plugin.
+ */
+ boolean installPlugin(PluginRelease pr);
+
+ /**
+ * The list of all registered plugins.
+ *
+ * @return a list of registered plugins
+ */
+ List<PluginRegistration> getRegisteredPlugins();
+
+ /**
+ * Lookup a plugin registration from the plugin registries.
+ *
+ * @param idOrName
+ * @return a plugin registration or null
+ */
+ PluginRegistration lookupPlugin(String idOrName);
+
+ /**
+ * Lookup a plugin release.
+ *
+ * @param idOrName
+ * @param version (use null for the current version)
+ * @return the identified plugin version or null
+ */
+ PluginRelease lookupRelease(String idOrName, String version);
}
*/
package com.gitblit.manager;
+import java.io.BufferedInputStream;
import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.fortsoft.pf4j.DefaultPluginManager;
+import ro.fortsoft.pf4j.PluginVersion;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.Keys;
+import com.gitblit.models.PluginRegistry;
+import com.gitblit.models.PluginRegistry.PluginRegistration;
+import com.gitblit.models.PluginRegistry.PluginRelease;
+import com.gitblit.utils.Base64;
import com.gitblit.utils.FileUtils;
+import com.gitblit.utils.JsonUtils;
+import com.gitblit.utils.StringUtils;
+import com.google.common.io.Files;
+import com.google.common.io.InputSupplier;
/**
* The plugin manager maintains the lifecycle of plugins. It is exposed as
* Dagger bean. The extension consumers supposed to retrieve plugin manager
* from the Dagger DI and retrieve extensions provided by active plugins.
- *
+ *
* @author David Ostrovsky
- *
+ *
*/
public class PluginManager extends DefaultPluginManager implements IPluginManager {
private final Logger logger = LoggerFactory.getLogger(getClass());
-
+
private final IRuntimeManager runtimeManager;
+ // timeout defaults of Maven 3.0.4 in seconds
+ private int connectTimeout = 20;
+
+ private int readTimeout = 12800;
+
public PluginManager(IRuntimeManager runtimeManager) {
super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"));
this.runtimeManager = runtimeManager;
stopPlugins();
return null;
}
-
+
@Override
public boolean deletePlugin(PluginWrapper pw) {
File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
File pluginFolder = new File(folder, pw.getPluginPath());
File pluginZip = new File(folder, pw.getPluginPath() + ".zip");
-
+
if (pluginFolder.exists()) {
FileUtils.delete(pluginFolder);
}
}
return true;
}
+
+ @Override
+ public boolean refreshRegistry() {
+ String dr = "http://gitblit.github.io/gitblit-registry/plugins.json";
+ String url = runtimeManager.getSettings().getString(Keys.plugins.registry, dr);
+ try {
+ return download(url);
+ } catch (Exception e) {
+ logger.error(String.format("Failed to retrieve plugins.json from %s", url), e);
+ }
+ return false;
+ }
+
+ protected List<PluginRegistry> getRegistries() {
+ List<PluginRegistry> list = new ArrayList<PluginRegistry>();
+ File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
+ FileFilter jsonFilter = new FileFilter() {
+ @Override
+ public boolean accept(File file) {
+ return !file.isDirectory() && file.getName().toLowerCase().endsWith(".json");
+ }
+ };
+
+ File [] files = folder.listFiles(jsonFilter);
+ if (files == null || files.length == 0) {
+ // automatically retrieve the registry if we don't have a local copy
+ refreshRegistry();
+ files = folder.listFiles(jsonFilter);
+ }
+
+ if (files == null || files.length == 0) {
+ return list;
+ }
+
+ for (File file : files) {
+ PluginRegistry registry = null;
+ try {
+ String json = FileUtils.readContent(file, "\n");
+ registry = JsonUtils.fromJsonString(json, PluginRegistry.class);
+ } catch (Exception e) {
+ logger.error("Failed to deserialize " + file, e);
+ }
+ if (registry != null) {
+ list.add(registry);
+ }
+ }
+ return list;
+ }
+
+ @Override
+ public List<PluginRegistration> getRegisteredPlugins() {
+ List<PluginRegistration> list = new ArrayList<PluginRegistration>();
+ Map<String, PluginRegistration> map = new TreeMap<String, PluginRegistration>();
+ for (PluginRegistry registry : getRegistries()) {
+ List<PluginRegistration> registrations = registry.registrations;
+ list.addAll(registrations);
+ for (PluginRegistration reg : registrations) {
+ reg.installedRelease = null;
+ map.put(reg.id, reg);
+ }
+ }
+ for (PluginWrapper pw : getPlugins()) {
+ String id = pw.getDescriptor().getPluginId();
+ PluginVersion pv = pw.getDescriptor().getVersion();
+ PluginRegistration reg = map.get(id);
+ if (reg != null) {
+ reg.installedRelease = pv.toString();
+ }
+ }
+ return list;
+ }
+
+ @Override
+ public PluginRegistration lookupPlugin(String idOrName) {
+ for (PluginRegistry registry : getRegistries()) {
+ PluginRegistration reg = registry.lookup(idOrName);
+ if (reg != null) {
+ return reg;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public PluginRelease lookupRelease(String idOrName, String version) {
+ for (PluginRegistry registry : getRegistries()) {
+ PluginRegistration reg = registry.lookup(idOrName);
+ if (reg != null) {
+ PluginRelease pv;
+ if (StringUtils.isEmpty(version)) {
+ pv = reg.getCurrentRelease();
+ } else {
+ pv = reg.getRelease(version);
+ }
+ if (pv != null) {
+ return pv;
+ }
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Installs the plugin from the plugin version.
+ *
+ * @param pv
+ * @throws IOException
+ * @return true if successful
+ */
+ @Override
+ public boolean installPlugin(PluginRelease pv) {
+ return installPlugin(pv.url);
+ }
+
+ /**
+ * Installs the plugin from the url.
+ *
+ * @param url
+ * @return true if successful
+ */
+ @Override
+ public boolean installPlugin(String url) {
+ try {
+ if (!download(url)) {
+ return false;
+ }
+ // TODO stop, unload, load
+ } catch (IOException e) {
+ logger.error("Failed to install plugin from " + url, e);
+ }
+ return true;
+ }
+
+ /**
+ * Download a file to the plugins folder.
+ *
+ * @param url
+ * @return
+ * @throws IOException
+ */
+ protected boolean download(String url) throws IOException {
+ File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
+ File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp");
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+
+ URL u = new URL(url);
+ final URLConnection conn = getConnection(u);
+
+ // try to get the server-specified last-modified date of this artifact
+ long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis());
+
+ Files.copy(new InputSupplier<InputStream>() {
+ @Override
+ public InputStream getInput() throws IOException {
+ return new BufferedInputStream(conn.getInputStream());
+ }
+ }, tmpFile);
+
+ File destFile = new File(pFolder, StringUtils.getLastPathElement(u.getPath()));
+ if (destFile.exists()) {
+ destFile.delete();
+ }
+ tmpFile.renameTo(destFile);
+ destFile.setLastModified(lastModified);
+
+ return true;
+ }
+
+ protected URLConnection getConnection(URL url) throws IOException {
+ java.net.Proxy proxy = getProxy(url);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy);
+ if (java.net.Proxy.Type.DIRECT != proxy.type()) {
+ String auth = getProxyAuthorization(url);
+ conn.setRequestProperty("Proxy-Authorization", auth);
+ }
+
+ String username = null;
+ String password = null;
+ if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
+ // set basic authentication header
+ String auth = Base64.encodeBytes((username + ":" + password).getBytes());
+ conn.setRequestProperty("Authorization", "Basic " + auth);
+ }
+
+ // configure timeouts
+ conn.setConnectTimeout(connectTimeout * 1000);
+ conn.setReadTimeout(readTimeout * 1000);
+
+ switch (conn.getResponseCode()) {
+ case HttpURLConnection.HTTP_MOVED_TEMP:
+ case HttpURLConnection.HTTP_MOVED_PERM:
+ // handle redirects by closing this connection and opening a new
+ // one to the new location of the requested resource
+ String newLocation = conn.getHeaderField("Location");
+ if (!StringUtils.isEmpty(newLocation)) {
+ logger.info("following redirect to {0}", newLocation);
+ conn.disconnect();
+ return getConnection(new URL(newLocation));
+ }
+ }
+
+ return conn;
+ }
+
+ protected Proxy getProxy(URL url) {
+ return java.net.Proxy.NO_PROXY;
+ }
+
+ protected String getProxyAuthorization(URL url) {
+ return "";
+ }
}
--- /dev/null
+/*\r
+ * Copyright 2014 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.models;\r
+\r
+import java.io.Serializable;\r
+import java.util.ArrayList;\r
+import java.util.Date;\r
+import java.util.List;\r
+\r
+import org.parboiled.common.StringUtils;\r
+\r
+import ro.fortsoft.pf4j.PluginVersion;\r
+\r
+/**\r
+ * Represents a list of plugin registrations.\r
+ */\r
+public class PluginRegistry implements Serializable {\r
+\r
+ private static final long serialVersionUID = 1L;\r
+\r
+ public final String name;\r
+\r
+ public final List<PluginRegistration> registrations;\r
+\r
+ public PluginRegistry(String name) {\r
+ this.name = name;\r
+ registrations = new ArrayList<PluginRegistration>();\r
+ }\r
+\r
+ public PluginRegistration lookup(String idOrName) {\r
+ for (PluginRegistration registration : registrations) {\r
+ if (registration.id.equalsIgnoreCase(idOrName)\r
+ || registration.name.equalsIgnoreCase(idOrName)) {\r
+ return registration;\r
+ }\r
+ }\r
+ return null;\r
+ }\r
+\r
+ @Override\r
+ public String toString() {\r
+ return getClass().getSimpleName();\r
+ }\r
+\r
+ public static enum InstallState {\r
+ NOT_INSTALLED, INSTALLED, CAN_UPDATE, UNKNOWN\r
+ }\r
+\r
+ /**\r
+ * Represents a plugin registration.\r
+ */\r
+ public static class PluginRegistration implements Serializable {\r
+\r
+ private static final long serialVersionUID = 1L;\r
+\r
+ public final String id;\r
+\r
+ public String name;\r
+\r
+ public String description;\r
+\r
+ public String provider;\r
+\r
+ public String projectUrl;\r
+\r
+ public String currentRelease;\r
+\r
+ public transient String installedRelease;\r
+\r
+ public List<PluginRelease> releases;\r
+\r
+ public PluginRegistration(String id) {\r
+ this.id = id;\r
+ this.releases = new ArrayList<PluginRelease>();\r
+ }\r
+\r
+ public PluginRelease getCurrentRelease() {\r
+ PluginRelease current = null;\r
+ if (!StringUtils.isEmpty(currentRelease)) {\r
+ current = getRelease(currentRelease);\r
+ }\r
+\r
+ if (current == null) {\r
+ Date date = new Date(0);\r
+ for (PluginRelease pv : releases) {\r
+ if (pv.date.after(date)) {\r
+ current = pv;\r
+ }\r
+ }\r
+ }\r
+ return current;\r
+ }\r
+\r
+ public PluginRelease getRelease(String version) {\r
+ for (PluginRelease pv : releases) {\r
+ if (pv.version.equalsIgnoreCase(version)) {\r
+ return pv;\r
+ }\r
+ }\r
+ return null;\r
+ }\r
+\r
+ public InstallState getInstallState() {\r
+ if (StringUtils.isEmpty(installedRelease)) {\r
+ return InstallState.NOT_INSTALLED;\r
+ }\r
+ PluginVersion ir = PluginVersion.createVersion(installedRelease);\r
+ PluginVersion cr = PluginVersion.createVersion(currentRelease);\r
+ switch (ir.compareTo(cr)) {\r
+ case -1:\r
+ return InstallState.UNKNOWN;\r
+ case 1:\r
+ return InstallState.CAN_UPDATE;\r
+ default:\r
+ return InstallState.INSTALLED;\r
+ }\r
+ }\r
+\r
+ @Override\r
+ public String toString() {\r
+ return id;\r
+ }\r
+ }\r
+\r
+ public static class PluginRelease {\r
+ public String version;\r
+ public Date date;\r
+ public String url;\r
+ }\r
+}\r
import java.util.List;
import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
import ro.fortsoft.pf4j.PluginDependency;
import ro.fortsoft.pf4j.PluginDescriptor;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.manager.IGitblit;
+import com.gitblit.models.PluginRegistry.PluginRegistration;
+import com.gitblit.models.PluginRegistry.PluginRelease;
import com.gitblit.models.UserModel;
import com.gitblit.utils.FlipTable;
import com.gitblit.utils.FlipTable.Borders;
register(user, StopPlugin.class);
register(user, ShowPlugin.class);
register(user, RemovePlugin.class);
- register(user, UploadPlugin.class);
+ register(user, InstallPlugin.class);
+ register(user, AvailablePlugins.class);
}
@CommandMetaData(name = "list", aliases = { "ls" }, description = "List the loaded plugins")
stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
}
-
+
@Override
protected void asTabbed(List<PluginWrapper> list) {
for (PluginWrapper pw : list) {
}
}
}
-
+
@CommandMetaData(name = "start", description = "Start a plugin")
public static class StartPlugin extends SshCommand {
}
}
}
-
+
protected void start(PluginWrapper pw) throws UnloggedFailure {
String id = pw.getDescriptor().getPluginId();
if (pw.getPluginState() == PluginState.STARTED) {
}
}
}
-
+
@CommandMetaData(name = "stop", description = "Stop a plugin")
public static class StopPlugin extends SshCommand {
}
}
}
-
+
protected void stop(PluginWrapper pw) throws UnloggedFailure {
String id = pw.getDescriptor().getPluginId();
if (pw.getPluginState() == PluginState.STOPPED) {
}
}
}
-
+
@CommandMetaData(name = "show", description = "Show the details of a plugin")
public static class ShowPlugin extends SshCommand {
String ext = exts.get(i);
data[0] = new Object[] { ext.toString(), ext.toString() };
}
- extensions = FlipTable.of(headers, data, Borders.COLS);
+ extensions = FlipTable.of(headers, data, Borders.COLS);
}
// DEPENDENCIES
PluginDependency dep = deps.get(i);
data[0] = new Object[] { dep.getPluginId(), dep.getPluginVersion() };
}
- dependencies = FlipTable.of(headers, data, Borders.COLS);
+ dependencies = FlipTable.of(headers, data, Borders.COLS);
}
-
+
String[] headers = { d.getPluginId() };
Object[][] data = new Object[5][];
data[0] = new Object[] { fields };
data[2] = new Object[] { extensions };
data[3] = new Object[] { "DEPENDENCIES" };
data[4] = new Object[] { dependencies };
- stdout.println(FlipTable.of(headers, data));
+ stdout.println(FlipTable.of(headers, data));
}
}
-
+
@CommandMetaData(name = "remove", aliases= { "rm", "del" }, description = "Remove a plugin", hidden = true)
public static class RemovePlugin extends SshCommand {
}
}
}
-
- @CommandMetaData(name = "receive", aliases= { "upload" }, description = "Upload a plugin to the server", hidden = true)
- public static class UploadPlugin extends SshCommand {
+
+ @CommandMetaData(name = "install", description = "Download and installs a plugin", hidden = true)
+ public static class InstallPlugin extends SshCommand {
+
+ @Argument(index = 0, required = true, metaVar = "<URL>|<ID>|<NAME>", usage = "the id, name, or the url of the plugin to download and install")
+ protected String urlOrIdOrName;
+
+ @Option(name = "--version", usage = "The specific version to install")
+ private String version;
@Override
public void run() throws UnloggedFailure {
+ IGitblit gitblit = getContext().getGitblit();
+ try {
+ String ulc = urlOrIdOrName.toLowerCase();
+ if (ulc.startsWith("http://") || ulc.startsWith("https://")) {
+ if (gitblit.installPlugin(urlOrIdOrName)) {
+ stdout.println(String.format("Installed %s", urlOrIdOrName));
+ } else {
+ new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName));
+ }
+ } else {
+ PluginRelease pv = gitblit.lookupRelease(urlOrIdOrName, version);
+ if (pv == null) {
+ throw new UnloggedFailure(1, String.format("Plugin \"%s\" is not in the registry!", urlOrIdOrName));
+ }
+ if (gitblit.installPlugin(pv)) {
+ stdout.println(String.format("Installed %s", urlOrIdOrName));
+ } else {
+ throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName));
+ }
+ }
+ } catch (Exception e) {
+ log.error("Failed to install " + urlOrIdOrName, e);
+ throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName), e);
+ }
+ }
+ }
+
+ @CommandMetaData(name = "available", description = "List the available plugins")
+ public static class AvailablePlugins extends ListFilterCommand<PluginRegistration> {
+
+ @Option(name = "--refresh", aliases = { "-r" }, usage = "refresh the plugin registry")
+ protected boolean refresh;
+
+ @Override
+ protected List<PluginRegistration> getItems() throws UnloggedFailure {
+ IGitblit gitblit = getContext().getGitblit();
+ if (refresh) {
+ gitblit.refreshRegistry();
+ }
+ List<PluginRegistration> list = gitblit.getRegisteredPlugins();
+ return list;
+ }
+
+ @Override
+ protected boolean matches(String filter, PluginRegistration t) {
+ return t.id.matches(filter) || t.name.matches(filter);
+ }
+
+ @Override
+ protected void asTable(List<PluginRegistration> list) {
+ String[] headers;
+ if (verbose) {
+ String [] h = { "Name", "Description", "Installed", "Release", "State", "Id", "Provider" };
+ headers = h;
+ } else {
+ String [] h = { "Name", "Description", "Installed", "Release", "State" };
+ headers = h;
+ }
+ Object[][] data = new Object[list.size()][];
+ for (int i = 0; i < list.size(); i++) {
+ PluginRegistration p = list.get(i);
+ if (verbose) {
+ data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState(), p.id, p.provider};
+ } else {
+ data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState()};
+ }
+ }
+
+ stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
+ }
+
+ @Override
+ protected void asTabbed(List<PluginRegistration> list) {
+ for (PluginRegistration p : list) {
+ if (verbose) {
+ outTabbed(p.name, p.description, p.currentRelease, p.getInstallState(), p.id, p.provider);
+ } else {
+ outTabbed(p.name, p.description, p.currentRelease, p.getInstallState());
+ }
+ }
}
}
}