From 99f4220a67739e4a3a76f57df62ab5ca8dc53ff4 Mon Sep 17 00:00:00 2001 From: Victoria Fierce Date: Sat, 3 Sep 2022 14:43:47 +0200 Subject: [PATCH] rewrite to be more dnf-like, with config editing --- README.md | 6 +- main.py | 267 +++++++++++++++++++++++++++++++++++++++++++++++++ model.py | 99 ++++++++++++++++++ plugin-sync.py | 191 ----------------------------------- plugins.yml | 183 --------------------------------- repo.py | 110 ++++++++++++++++++++ server.py | 96 ++++++++++++++++++ 7 files changed, 575 insertions(+), 377 deletions(-) create mode 100755 main.py create mode 100644 model.py delete mode 100755 plugin-sync.py delete mode 100644 plugins.yml create mode 100644 repo.py create mode 100644 server.py diff --git a/README.md b/README.md index b781fb1..2546d78 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# A tool to manage plugins on play.malloc.gg +# Minecraft Plugin Sync -plugin-sync is a tool used to maintain the various servers and their plugins on +mpm is a tool used to maintain the various servers and their plugins on play.malloc.gg. Malloc runs a heterogenous environment of server versions, depending on what @@ -12,7 +12,7 @@ tool to help out the workload. ## Use cases -plugin-sync is highly specialized for the purposes of malloc.gg, but it should +mpm is highly specialized for the purposes of malloc.gg, but it should be able to work in other systems where: - You have multiple servers in a network diff --git a/main.py b/main.py new file mode 100755 index 0000000..c601326 --- /dev/null +++ b/main.py @@ -0,0 +1,267 @@ +#!/bin/env python +import argparse +import pathlib +import sys +import yaml +import os + +from repo import Repo +from server import Server +from model import * + +try: + from yaml import CLoader as Loader, CDumper as Dumper +except ImportError: + from yaml import Loader, Dumper + +DEFAULT_CONFIG = """ +repositories: {} +servers: {} +""" + +class Config(): + + def __init__(self, path): + if path is None: + path = './mpm.yaml' + self.path = path + with open(path, 'r') as fd: + self.yaml = yaml.load(fd, Loader=Loader) + self.config = yaml.load(DEFAULT_CONFIG, Loader=Loader) + if isinstance(self.yaml, dict): + self.config.update(self.yaml) + + def repository(self, name): + return Repo(name, self.config['repositories'][name]) + + def repositories(self): + return [Repo(name, c) for (name, c) in self.config['repositories'].items()] + + def update_repository(self, name, config): + self.config['repositories'][name] = config + + def add_repository(self, name, path): + if name in self.config['repositories']: + raise ValueError('Repository already exists') + + self.update_repository(name, { + 'path': path + }) + + def servers(self): + return [Server(name, c) for (name, c) in self.config['servers'].items()] + + def server(self, name): + return Server(name, self.config['servers'][name]) + + def update_server(self, server, config): + self.config['servers'][server] = config + + def add_server(self, name, path): + if name in self.config['servers']: + raise ValueError("Server already exists") + + self.update_server(name, { + 'path': path, + 'plugins': [], + 'inherit': [] + }) + + def save(self): + stream = open(self.path, 'w') + yaml.dump(self.config, stream, Dumper=Dumper) + stream.close() + +def do_repo_add(args, config): + if not os.path.exists(args.path): + os.makedirs(args.path) + config.add_repository(args.name, args.path) + config.save() + print("Added repository {}".format(args.path)) + +def do_repo_list(args, config): + for repo in config.repositories(): + print("{} ({})".format(repo.name, repo.path)) + for plugin in sorted(repo.plugins()): + print('\t', plugin.name, '\t', plugin.version) + for badFile in sorted(repo.badFiles()): + print('\tWARNING: Unknown file', badFile) + +def do_repo_import(args, config): + repo = config.repository(args.name) + plugins = [] + for path in args.path: + try: + plugins.append(Plugin(path)) + except: + print("Bad plugin filename {}".format(path)) + + if len(plugins) == 0: + print("No plugins found.") + + print('Found the following plugins:') + for plugin in plugins: + print("\t{} {}".format(plugin.name, plugin.version)) + print("Import plugins into {}? [y/N]".format(repo.name)) + answer = input().lower() + if answer == "y": + for plugin in plugins: + repo.importPlugin(plugin) + print("Imported!") + else: + print("Cancelled.") + +def do_server_add(args, config): + config.add_server(args.name, args.path) + config.save() + print("Added server {} in {}".format(args.name, args.path)) + +def do_server_list(args, config): + for server in config.servers(): + print('{} ({}):'.format(server.name, server.path)) + outdatedLinks = [] + missing = [] + installed = [] + unmanaged = [] + conflicts = [] + for state in sorted(server.pluginStates(config.repositories())): + if isinstance(state, OutdatedSymlink): + outdatedLinks.append(state) + elif isinstance(state, Installed): + installed.append(state) + elif isinstance(state, MissingVersions): + missing.append(state) + elif isinstance(state, UnmanagedFile): + unmanaged.append(state) + elif isinstance(state, SymlinkConflict): + conflicts.append(state) + + print("Installed plugins:") + for state in sorted(installed): + print("\t{} {}: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion)) + print("Oudated symlinks:") + for state in sorted(outdatedLinks): + print("\t{} {}: Current: {} Wanted: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion, state.wantedVersion)) + print("Missing plugins:") + for state in sorted(missing): + print("\t{}: {}".format(state.plugin.name, state.plugin.versionSpec)) + print("Unmanaged files:") + for state in sorted(unmanaged): + print("\t{}".format(state.filename)) + print("Symlink Conflicts:") + for state in sorted(conflicts): + print("\t{}.jar".format(state.plugin.name)) + +def do_server_add_plugin(args, config): + server = config.server(args.server) + plugins = [] + for pluginSpec in args.plugin: + + if os.path.exists(pluginSpec): + plugin = Plugin(pluginSpec) + pluginSpec = PluginSpec(plugin.name, str(plugin.version)) + else: + allVersions = [] + for repo in config.repositories(): + allVersions += repo.versionsForPlugin(pluginSpec) + pluginSpec = PluginSpec(pluginSpec, list(reversed(sorted(allVersions)))[0]) + + plugins.append(pluginSpec) + + print("Added {} to {}".format(pluginSpec, server.name)) + for pluginSpec in plugins: + print("\t{} {}".format(pluginSpec.name, pluginSpec.versionSpec)) + print("Add these plugins to server {}? [y/N]".format(server.name)) + answer = input().lower() + if answer == "y": + for pluginSpec in plugins: + server.add_plugin(pluginSpec) + config.update_server(server.name, server.config) + config.save() + print("Added!") + else: + print("Cancelled.") + +def do_server_sync(args, config): + for server in config.servers(): + print('{} ({}):'.format(server.name, server.path)) + outdatedLinks = [] + available = [] + for state in sorted(server.pluginStates(config.repositories())): + if isinstance(state, OutdatedSymlink): + outdatedLinks.append(state) + elif isinstance(state, Available): + available.append(state) + + print("Plugins to update:") + for state in sorted(outdatedLinks): + print("\t{} {}: Current: {} Wanted: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion, state.wantedVersion)) + print("New plugins to install:") + for state in sorted(available): + print("\t{}: {}".format(state.plugin.name, state.plugin.version)) + + if len(outdatedLinks) > 0 or len(available) > 0: + print("Apply changes? [y/N]") + answer = input().lower() + if answer == "y": + for state in available: + server.installVersion(state.plugin) + server.updateSymlinkForPlugin(state.plugin, state.plugin.version) + print("Installed {} {}".format(state.plugin.name, state.plugin.version)) + for state in outdatedLinks: + server.updateSymlinkForPlugin(state.plugin, state.wantedVersion) + print("Updated {} to {}".format(state.plugin.name, state.wantedVersion)) + else: + print("Not applying changes.") + else: + print("No changes to apply.") + +def main(): + parser = argparse.ArgumentParser(description='Paper Plugin Sync') + parser.add_argument('--config', dest='config_path', type=Config) + subparsers = parser.add_subparsers() + repos = subparsers.add_parser('repo') + repo_sub = repos.add_subparsers() + + repo_add = repo_sub.add_parser('add') + repo_add.add_argument('name', help='Name of the repository') + repo_add.add_argument('path', help='Where to add a repository or create a new one') + repo_add.set_defaults(func=do_repo_add) + + repo_list = repo_sub.add_parser('list') + repo_list.set_defaults(func=do_repo_list) + + repo_import = repo_sub.add_parser('import') + repo_import.add_argument('name', help='Name of the repository') + repo_import.add_argument('path', nargs="+", help='Path of the plugin to import') + repo_import.set_defaults(func=do_repo_import) + + servers = subparsers.add_parser('server') + server_sub = servers.add_subparsers() + server_add = server_sub.add_parser('add') + server_add.add_argument('name', help='Name for the server') + server_add.add_argument('path', help='Path to your server\'s root directory') + server_add.set_defaults(func=do_server_add) + + server_list = server_sub.add_parser('list') + server_list.set_defaults(func=do_server_list) + + server_add_plugin = server_sub.add_parser('add-plugin') + server_add_plugin.add_argument('server', help='Name of server to modify') + server_add_plugin.add_argument('plugin', nargs='+', help='Plugin file or spec to install') + server_add_plugin.set_defaults(func=do_server_add_plugin) + + server_sync = server_sub.add_parser('sync') + server_sync.set_defaults(func=do_server_sync) + + args = parser.parse_args() + + config = Config(args.config_path) + + if 'func' not in args: + parser.print_usage() + else: + args.func(args, config) + +if __name__ == "__main__": + main() diff --git a/model.py b/model.py new file mode 100644 index 0000000..c9ee5a0 --- /dev/null +++ b/model.py @@ -0,0 +1,99 @@ +from semantic_version import Spec, Version +from functools import total_ordering +import os +import re + +#version_pattern = re.compile('^(?P.*)-(?P[^-]+)(?P-[^-]+)?\.jar$') +version_pattern = re.compile('^(?P.*)-(?P[^-]+(?:-[^-]+)?)\.jar$') +version_pattern = re.compile('^(?P.+)-(?P(?:\.?\d+)+).+jar$') + +@total_ordering +class Plugin: + def __init__(self, path): + self.path = path + pluginName = os.path.basename(path) + pluginMatches = version_pattern.match(pluginName) + + if pluginMatches is None: + raise ValueError("Cannot derive plugin name from '{}'".format(path)) + + self.name = pluginMatches['name'] + + try: + self.version = Version.coerce(pluginMatches['version']) + except ValueError: + raise ValueError("Cannot derive semver from '{}'".format(path)) + + def __eq__(self, other): + return self.name == other.name and self.version == other.version + + def __ne__(self, other): + return self.name != other.name or self.version != other.version + + def __lt__(self, other): + if self.name == other.name: + return self.version < other.version + return self.name < other.name + +@total_ordering +class PluginSpec: + def __init__(self, name, versionSpec): + self.name = name + try: + self.versionSpec = Spec(str(versionSpec)) + except ValueError: + raise ValueError("Invalid version spec for plugin {}: {}".format(name, versionSpec)) + + def __str__(self): + return "{} {}".format(self.name, self.versionSpec) + + def __eq__(self, other): + return self.name == other.name and self.versionSpec == other.versionSpec + + def __ne__(self, other): + return self.name != other.name or self.versionSpec != other.versionSpec + + def __lt__(self, other): + return self.name < other.name + +@total_ordering +class PluginState: + def __init__(self, plugin): + self.plugin = plugin + + def __eq__(self, other): + return self.plugin == other.plugin + + def __ne__(self, other): + return self.plugin != other.plugin + + def __lt__(self, other): + return self.plugin.name < other.plugin.name + +class UnmanagedFile(PluginState): + def __init__(self, filename): + self.filename = filename + + def __lt__(self, other): + return self.filename < other.filename + +class OutdatedSymlink(PluginState): + def __init__(self, plugin, currentVersion, wantedVersion): + super().__init__(plugin) + self.currentVersion = currentVersion + self.wantedVersion = wantedVersion + +class SymlinkConflict(PluginState): + pass + +class MissingVersions(PluginState): + pass + +class Available(PluginState): + def __init__(self, repoPlugin): + super().__init__(repoPlugin) + +class Installed(PluginState): + def __init__(self, plugin, currentVersion): + super().__init__(plugin) + self.currentVersion = currentVersion diff --git a/plugin-sync.py b/plugin-sync.py deleted file mode 100755 index dc3358f..0000000 --- a/plugin-sync.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/bin/env python -import yaml -from semantic_version import Spec, Version -import os -import sys -from functools import total_ordering - -try: - from yaml import CLoader as Loader, CDumper as Dumper -except ImportError: - from yaml import Loader, Dumper - -conf = yaml.load(open('plugins.yml', 'r'), Loader=Loader) - -class PluginSpec: - def __init__(self, name, versionSpec): - self.name = name - try: - self.versionSpec = Spec(versionSpec) - except ValueError: - raise ValueError("Invalid version spec for plugin {}: {}".format(name, versionSpec)) - -@total_ordering -class PluginState: - def __init__(self, plugin): - self.plugin = plugin - - def __eq__(self, other): - return self.plugin == other.plugin - - def __ne__(self, other): - return self.plugin != other.plugin - - def __lt__(self, other): - return self.plugin.name < other.plugin.name - -class UnmanagedFile(PluginState): - def __init__(self, filename): - self.filename = filename - - def __lt__(self, other): - return self.filename < other.filename - -class OutdatedSymlink(PluginState): - def __init__(self, plugin, currentVersion, wantedVersion): - super().__init__(plugin) - self.currentVersion = currentVersion - self.wantedVersion = wantedVersion - -class SymlinkConflict(PluginState): - pass - -class MissingVersions(PluginState): - pass - -class Installed(PluginState): - def __init__(self, plugin, currentVersion): - super().__init__(plugin) - self.currentVersion = currentVersion - -class Repo: - def __init__(self, path, pluginSet): - self.path = path - self.pluginSet = pluginSet - - def updateSymlinkForPlugin(self, plugin, version): - pluginFilename = os.path.join(self.path, 'versions/{}-{}.jar'.format(plugin.name, version)) - pluginSymlink = os.path.join(self.path, plugin.name + '.jar') - linkDst = os.path.relpath(pluginFilename, self.path) - - if os.path.lexists(pluginSymlink): - os.unlink(pluginSymlink) - os.symlink(linkDst, pluginSymlink) - - def pluginStates(self): - managedPluginFilenames = [] - for plugin in self.pluginSet: - compatibleVersions = [] - pluginLinkName = '{}.jar'.format(plugin.name) - managedPluginFilenames.append(pluginLinkName) - - if os.path.exists(os.path.join(self.path, pluginLinkName)) and not os.path.islink(os.path.join(self.path, pluginLinkName)): - yield SymlinkConflict(plugin) - continue - - for installedVersion in self.versionsForPlugin(plugin.name): - if installedVersion in plugin.versionSpec: - compatibleVersions.append(installedVersion) - - if len(compatibleVersions) == 0: - yield MissingVersions(plugin) - else: - preferredVersion = list(reversed(sorted(compatibleVersions)))[0] - currentVersion = self.currentVersionForPlugin(plugin.name) - - if currentVersion == preferredVersion: - yield Installed(plugin, currentVersion) - else: - yield OutdatedSymlink(plugin, currentVersion, preferredVersion) - - otherPlugins = os.listdir(self.path) - for pluginFile in otherPlugins: - if os.path.isfile(os.path.join(self.path, pluginFile)) and pluginFile not in managedPluginFilenames: - yield UnmanagedFile(pluginFile) - - def currentVersionForPlugin(self, pluginName): - pluginSymlink = os.path.join(self.path, pluginName + '.jar') - if not os.path.lexists(pluginSymlink): - return None - suffix = '.jar' - pluginJar = os.path.basename(os.readlink(pluginSymlink)) - jarVersion = pluginJar[len(pluginName)+1:len(pluginJar)-len(suffix)] - try: - pluginSemver = Version.coerce(jarVersion) - except ValueError: - pluginSemver = jarVersion - return pluginSemver - - def versionsForPlugin(self, pluginName): - plugins = os.listdir(os.path.join(self.path, 'versions')) - for pluginJar in plugins: - if pluginJar.startswith(pluginName): - prefix = pluginName + '-' - suffix = '.jar' - jarVersion = pluginJar[len(prefix):len(pluginJar)-len(suffix)] - try: - pluginSemver = Version.coerce(jarVersion) - except ValueError: - pluginSemver = jarVersion - yield pluginSemver - -for (serverName,server) in conf['servers'].items(): - changeset = [] - if len(sys.argv) > 1 and serverName not in sys.argv[1:]: - continue - if 'pluginPath' not in server: - continue - if not os.path.exists(server['pluginPath']): - print("Missing plugin path for {}: {}".format(serverName, server['pluginPath'])) - else: - print("=== Updating server {}".format(serverName)) - pluginSpecs = {} - for inherited in server.get('inherit', ()): - for inheritedPlugin in conf['servers'][inherited]['plugins']: - pluginSpecs[inheritedPlugin['name']] = PluginSpec(inheritedPlugin['name'], str(inheritedPlugin.get('version', '*'))) - - for pluginConf in server.get('plugins', ()): - pluginSpecs[pluginConf['name']] = PluginSpec(pluginConf['name'], str(pluginConf.get('version', '*'))) - - repo = Repo(server['pluginPath'], pluginSpecs.values()) - outdatedLinks = [] - missing = [] - installed = [] - unmanaged = [] - conflicts = [] - for state in repo.pluginStates(): - if isinstance(state, OutdatedSymlink): - outdatedLinks.append(state) - elif isinstance(state, Installed): - installed.append(state) - elif isinstance(state, MissingVersions): - missing.append(state) - elif isinstance(state, UnmanagedFile): - unmanaged.append(state) - elif isinstance(state, SymlinkConflict): - conflicts.append(state) - - print("Installed plugins:") - for state in sorted(installed): - print("\t{} {}: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion)) - print("Oudated symlinks:") - for state in sorted(outdatedLinks): - print("\t{} {}: Current: {} Wanted: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion, state.wantedVersion)) - print("Missing plugins:") - for state in sorted(missing): - print("\t{}: {}".format(state.plugin.name, state.plugin.versionSpec)) - print("Unmanaged files:") - for state in sorted(unmanaged): - print("\t{}".format(state.filename)) - print("Symlink Conflicts:") - for state in sorted(conflicts): - print("\t{}.jar".format(state.plugin.name)) - - if len(outdatedLinks) > 0: - print("Apply changes? [y/N]") - answer = input().lower() - if answer == "y": - for state in outdatedLinks: - repo.updateSymlinkForPlugin(state.plugin, state.wantedVersion) - else: - print("Not applying changes.") diff --git a/plugins.yml b/plugins.yml deleted file mode 100644 index be4e896..0000000 --- a/plugins.yml +++ /dev/null @@ -1,183 +0,0 @@ -servers: - 1.17-smp: - plugins: - - name: Pl3xMap - - name: pl3xmap-signs - 1.18-common: - plugins: - - name: voicechat-bukkit-1.18.2 - version: 2.2.28 - 1.17-common: - plugins: - - name: voicechat-bukkit-1.17.1 - version: 2.2.28 - smp-common: - plugins: - - name: ActionHealth - creative: - inherit: - - common - - 1.17-common - pluginPath: /srv/minecraft/creative/plugins/ - plugins: - - name: HolographicDisplays - - name: HolographicExtension - - name: EssentialsXSpawn - - name: WorldSystem - version: 2.4.12 - villager-defense: - inherit: - - common - - 1.17-common - pluginPath: /srv/minecraft/villager-defense/plugins/ - mcdev: - inherit: - - common - - smp-common - - 1.18-common - - SMP - pluginPath: /srv/minecraft/mcdev/plugins/ - plugins: - - name: tabtps-spigot - version: '*' - - name: item-nbt-api-plugin - version: 2.9.2 - - name: QuickShop - version: 5 - - name: AngelChest - version: '*' - - name: GameModeInventories - version: '*' - SMP: - inherit: - - common - - smp-common - - 1.17-common - - 1.17-smp - pluginPath: /srv/minecraft/server/plugins/ - plugins: - - name: ATMSigns - - name: Harbor - - name: BetterRTP - - name: CoreProtect - version: 19.5.* - - name: Chunky - version: 1.2.86 - - name: ChunkyBorder - version: 1.0.38 - - name: GameModeInventories - version: 3.3.3 - - name: HolographicDisplays - version: 2 - - name: Regions - version: '0.3' - - name: Shopkeepers - version: 2.13.2 - - name: Lands - version: 5.11.18 - - name: OpenInv - version: 4.1.8 - - name: AngelChest - version: 4.2.0 - - name: PvPManager - version: 3 - - name: EssentialsXSpawn - version: 2 - - name: MythicMobs - version: 5.0.0-alpha1 - - name: Model-Engine - version: 2.* - - name: LibsDisguises - version: 10 - - name: QuickShop - version: 4.0 - - name: ProtocolLib - version: 4.8.0 - skyblock: - inherit: - - 1.18-common - - common - pluginPath: /srv/minecraft/skyblock/plugins/ - plugins: - - name: BentoBox - version: 1.19.0 - lobby: - pluginPath: /srv/minecraft/lobby/plugins/ - inherit: - - common - plugins: - - name: ItemJoin - - name: Parkour - version: 6.7.1 - - name: GadgetsMenu - version: 5.0.2 - - name: Denizen - version: 1.2.1-b1743-REL - - name: JumpPads - version: 1.25.6 - - name: VoidSpawn - version: 1.0.1 - - name: EssentialsXSpawn - version: 2.18.2 - - name: HolographicExtension - version: 1.10.9 - - name: HolographicDisplays - version: 2.4.9 - - name: Citizens - version: 2.0.28-5 - - name: voicechat-bukkit-1.17.1 - version: 2.2.28 - common: - plugins: - - name: Advanced-Portals - version: 0.7.1 - - name: DeluxeMenus - version: 1.13.3 - - name: AsyncWorldEdit - version: 3.9.1 - - name: CoordinatesHUD - - name: BungeeTabListPlus_BukkitBridge - version: ~3.4 - - name: BuycraftX - - name: DBVerifier - version: 1.6.5-malloc - - name: DiscordSRV - version: 1.24.0 - - name: EssentialsX - version: 2.18.2 - - name: GSit - version: 6.0.2 - - name: item-nbt-api-plugin - version: 2.* - - name: LuckPerm-Bukkit - version: 5.4.9 - - name: PlaceholderAPI - version: 2.10.9 - - name: Plan - version: 5.4.0-build-1354 - - name: PlayerPoints - version: 3.0.3 - - name: ProtocolLib - version: 4.8.0 - - name: SuperVanish - version: 6.2.6 - - name: tabtps-spigot - version: 1.3.8 - - name: Umfrage - version: 1.6.3-STABLE - - name: Vault - - name: VentureChat - version: 3.3.2 - - name: floodgate-bukkit - version: 53 - - name: DisableJoinMessage - - name: worldedit-bukkit - - name: worldguard-bukkit - - name: SkQuery - version: 4.1.4 - - name: Skript - version: 2.6.0-beta2 - - name: skUtilities - version: 0.9.2 - - name: Skungee - - name: NametagEdit diff --git a/repo.py b/repo.py new file mode 100644 index 0000000..5141a37 --- /dev/null +++ b/repo.py @@ -0,0 +1,110 @@ +import os +from model import * +import shutil + +class Repo: + def __init__(self, name, config): + self.name = name + self.config = config + self.path = config['path'] + + def importPlugin(self, plugin): + dest = "{}/{}-{}.jar".format(self.path, plugin.name, plugin.version) + shutil.copyfile(plugin.path, dest) + + def plugins(self): + plugins = os.listdir(self.path) + for pluginFile in plugins: + fullPath = os.path.join(self.path, pluginFile) + if os.path.isfile(fullPath): + try: + yield Plugin(fullPath) + except ValueError: + continue + + def versionsForPlugin(self, name): + for plugin in self.plugins(): + if plugin.name == name: + yield plugin.version + + def badFiles(self): + plugins = os.listdir(self.path) + for pluginFile in plugins: + fullPath = os.path.join(self.path, pluginFile) + if os.path.isfile(fullPath): + try: + Plugin(fullPath) + except ValueError: + yield fullPath + +class Server: + def __init__(self, path, pluginSet): + self.path = path + self.pluginSet = pluginSet + + def updateSymlinkForPlugin(self, plugin, version): + pluginFilename = os.path.join(self.path, 'plugins/versions/{}-{}.jar'.format(plugin.name, version)) + pluginSymlink = os.path.join(self.path, plugin.name + '.jar') + linkDst = os.path.relpath(pluginFilename, self.path) + + if os.path.lexists(pluginSymlink): + os.unlink(pluginSymlink) + os.symlink(linkDst, pluginSymlink) + + def pluginStates(self): + managedPluginFilenames = [] + for plugin in self.pluginSet: + compatibleVersions = [] + pluginLinkName = '{}.jar'.format(plugin.name) + managedPluginFilenames.append(pluginLinkName) + + if os.path.exists(os.path.join(self.path, pluginLinkName)) and not os.path.islink(os.path.join(self.path, pluginLinkName)): + yield SymlinkConflict(plugin) + continue + + for installedVersion in self.versionsForPlugin(plugin.name): + if installedVersion in plugin.versionSpec: + compatibleVersions.append(installedVersion) + + if len(compatibleVersions) == 0: + yield MissingVersions(plugin) + else: + preferredVersion = list(reversed(sorted(compatibleVersions)))[0] + currentVersion = self.currentVersionForPlugin(plugin.name) + + if currentVersion == preferredVersion: + yield Installed(plugin, currentVersion) + else: + yield OutdatedSymlink(plugin, currentVersion, preferredVersion) + + otherPlugins = os.listdir(self.path) + for pluginFile in otherPlugins: + if os.path.isfile(os.path.join(self.path, pluginFile)) and pluginFile not in managedPluginFilenames: + yield UnmanagedFile(pluginFile) + + def currentVersionForPlugin(self, pluginName): + pluginSymlink = os.path.join(self.path, pluginName + '.jar') + if not os.path.lexists(pluginSymlink): + return None + suffix = '.jar' + pluginJar = os.path.basename(os.readlink(pluginSymlink)) + jarVersion = pluginJar[len(pluginName)+1:len(pluginJar)-len(suffix)] + try: + pluginSemver = Version.coerce(jarVersion) + except ValueError: + pluginSemver = jarVersion + return pluginSemver + + def versionsForPlugin(self, pluginName): + plugins = os.listdir(os.path.join(self.path, 'plugins', 'versions')) + for pluginJar in plugins: + if pluginJar.startswith(pluginName): + prefix = pluginName + '-' + suffix = '.jar' + jarVersion = pluginJar[len(prefix):len(pluginJar)-len(suffix)] + try: + pluginSemver = Version.coerce(jarVersion) + except ValueError: + pluginSemver = jarVersion + yield pluginSemver + diff --git a/server.py b/server.py new file mode 100644 index 0000000..3d03528 --- /dev/null +++ b/server.py @@ -0,0 +1,96 @@ +from model import * +import shutil + +class Server: + def __init__(self, name, config): + self.name = name + self.config = config + self.path = config['path'] + self.pluginPath = self.path+'/plugins' + + def plugins(self): + return [PluginSpec(p['name'], p['version']) for p in self.config['plugins']] + + def add_plugin(self, pluginSpec): + for plugin in self.config['plugins']: + if plugin['name'] == pluginSpec.name: + raise KeyError("Cannot add plugin multiple times.") + self.config['plugins'].append({'name': pluginSpec.name, 'version': str(pluginSpec.versionSpec)}) + + def pluginStates(self, repos): + managedPluginFilenames = [] + for plugin in self.plugins(): + compatibleVersions = [] + pluginLinkName = '{}.jar'.format(plugin.name) + managedPluginFilenames.append(pluginLinkName) + + if os.path.exists(os.path.join(self.pluginPath, pluginLinkName)) and not os.path.islink(os.path.join(self.pluginPath, pluginLinkName)): + yield SymlinkConflict(plugin) + continue + + for installedVersion in self.versionsForPlugin(plugin.name, repos): + if installedVersion in plugin.versionSpec: + compatibleVersions.append(installedVersion) + + if len(compatibleVersions) == 0: + for repo in repos: + for repoPlugin in repo.plugins(): + if repoPlugin.name == plugin.name and repoPlugin.version in plugin.versionSpec: + compatibleVersions.append(repoPlugin) + if len(compatibleVersions) == 0: + yield MissingVersions(plugin) + else: + preferredVersion = list(reversed(sorted(compatibleVersions)))[0] + yield Available(preferredVersion) + else: + preferredVersion = list(reversed(sorted(compatibleVersions)))[0] + currentVersion = self.currentVersionForPlugin(plugin.name) + + if currentVersion == preferredVersion: + yield Installed(plugin, currentVersion) + else: + yield OutdatedSymlink(plugin, currentVersion, preferredVersion) + + otherPlugins = os.listdir(self.pluginPath) + for pluginFile in otherPlugins: + if os.path.isfile(os.path.join(self.pluginPath, pluginFile)) and pluginFile not in managedPluginFilenames: + yield UnmanagedFile(pluginFile) + + def currentVersionForPlugin(self, pluginName): + pluginSymlink = os.path.join(self.pluginPath, pluginName + '.jar') + if not os.path.lexists(pluginSymlink): + return None + suffix = '.jar' + pluginJar = os.path.basename(os.readlink(pluginSymlink)) + jarVersion = pluginJar[len(pluginName)+1:len(pluginJar)-len(suffix)] + try: + pluginSemver = Version.coerce(jarVersion) + except ValueError: + pluginSemver = jarVersion + return pluginSemver + + def versionsForPlugin(self, pluginName, repos): + plugins = os.listdir(os.path.join(self.pluginPath, 'versions')) + for pluginJar in plugins: + if pluginJar.startswith(pluginName): + prefix = pluginName + '-' + suffix = '.jar' + jarVersion = pluginJar[len(prefix):len(pluginJar)-len(suffix)] + try: + pluginSemver = Version.coerce(jarVersion) + except ValueError: + pluginSemver = jarVersion + yield pluginSemver + + def updateSymlinkForPlugin(self, plugin, version): + pluginFilename = os.path.join(self.pluginPath, 'versions/{}-{}.jar'.format(plugin.name, version)) + pluginSymlink = os.path.join(self.pluginPath, plugin.name + '.jar') + linkDst = os.path.relpath(pluginFilename, self.pluginPath) + + if os.path.lexists(pluginSymlink): + os.unlink(pluginSymlink) + os.symlink(linkDst, pluginSymlink) + + def installVersion(self, plugin): + dest = os.path.join(self.pluginPath, 'versions/{}-{}.jar'.format(plugin.name, plugin.version)) + shutil.copyfile(plugin.path, dest)