Initial commit (of mostly all of it...)

This commit is contained in:
Josh Stark 2019-03-10 18:03:08 +00:00
parent 0f2954afea
commit ff7c510bb5
116 changed files with 11241 additions and 0 deletions

6
.gitignore vendored
View File

@ -21,3 +21,9 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
.idea/
build/
out/
*.iml
config/fleet.properties

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
#Mon Nov 19 20:32:01 GMT 2018
gradle.version=4.9

Binary file not shown.

View File

41
build.gradle Normal file
View File

@ -0,0 +1,41 @@
plugins {
id 'java'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
// Logging
runtime 'org.apache.logging.log4j:log4j-api:2.11.1'
runtime 'org.apache.logging.log4j:log4j-core:2.11.1'
compile 'org.apache.logging.log4j:log4j-slf4j-impl:2.11.1'
// HTTP Framework
compile 'com.sparkjava:spark-core:2.7.2'
compile 'com.sparkjava:spark-template-freemarker:2.7.1'
compile 'org.apache.httpcomponents:httpclient:4.5.7'
// JSON Mapping/Marshalling
compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6'
compile 'com.fasterxml.jackson.core:jackson-core:2.9.6'
compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.6'
// Database
compile 'com.zaxxer:HikariCP:3.3.1'
compile 'org.mariadb.jdbc:mariadb-java-client:2.4.0'
compile 'org.flywaydb:flyway-core:6.0.0-beta'
// MISC
compile 'org.apache.commons:commons-lang3:3.7'
// Unit Testing
testCompile 'junit:junit:4.11'
testCompile 'org.mockito:mockito-all:1.10.19'
}

View File

@ -0,0 +1,21 @@
# This is an example properties file for Fleet.
# You should fill in your own values here and rename the file to "fleet.properties".
# Runtime
fleet.app.port=8080
fleet.refresh.interval=60
# User for management of images and repositories
# CHANGE THESE!!
fleet.admin.username=test
fleet.admin.password=test
# Database Connectivity
fleet.database.driver=org.mariadb.jdbc.Driver
fleet.database.url=jdbc:mariadb://<IP_OR_URL>:3306/fleet
fleet.database.username=<fleet_sql_user>
fleet.database.password=<fleet_sql_password>
# Docker Hub
fleet.dockerhub.username=<username_for_your_dockerhub_account>
fleet.dockerhub.password=<password_for_your_dockerhub_account>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

172
gradlew vendored Executable file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View File

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth;
public class AuthenticatedUser {
private final String name;
public AuthenticatedUser(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth;
public class AuthenticationResult {
private final boolean authenticated;
private final AuthenticatedUser user;
public AuthenticationResult(boolean authenticated, AuthenticatedUser user) {
this.authenticated = authenticated;
this.user = user;
}
public static AuthenticationResult notAuthenticated() {
return new AuthenticationResult(false, null);
}
public boolean isAuthenticated() {
return authenticated;
}
public AuthenticatedUser getUser() {
return user;
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* Prints out all runtime arguments passed in as JVM arguments (<i>-D</i>).
* </p>
*
* @author Josh Stark
*/
abstract class BaseRuntimeLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(BaseRuntimeLoader.class);
BaseRuntimeLoader() {
LOGGER.info("Initalising...");
LOGGER.info("Config base : " + FleetRuntime.CONFIG_BASE);
LOGGER.info("Show Passwords : " + FleetRuntime.SHOW_PASSWORDS);
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.core;
import io.linuxserver.fleet.thread.SynchroniseAllRepositoriesTask;
import io.linuxserver.fleet.web.pages.HomePage;
import io.linuxserver.fleet.web.pages.LoginPage;
import io.linuxserver.fleet.web.pages.ManageRepositoriesPage;
import io.linuxserver.fleet.web.routes.*;
import java.util.concurrent.TimeUnit;
/**
* <p>
* Primary entry point for the application. All contexts and resources are loaded
* through this class.
* </p>
*/
class FleetApp {
private final FleetBeans beans;
FleetApp() {
beans = new FleetBeans();
}
void run() {
migrateDatabase();
configureWeb();
scheduleSync();
// beans.getSynchronisationDelegate().synchronise();
}
private void migrateDatabase() {
beans.getDatabaseVersion().migrate();
}
private void configureWeb() {
beans.getWebServer().start();
beans.getWebServer().addPage("/", new HomePage(beans.getRepositoryDelegate(), beans.getImageDelegate()));
beans.getWebServer().addPage("/admin", new ManageRepositoriesPage(beans.getRepositoryDelegate()));
beans.getWebServer().addPage("/admin/login", new LoginPage());
beans.getWebServer().addPostRoute("/admin/login", new LoginRoute(beans.getAuthenticationDelegate()));
beans.getWebServer().addPostRoute("/admin/logout", new LogoutRoute());
beans.getWebServer().addGetApi("/api/v1/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()));
beans.getWebServer().addPostApi("/admin/manageImage", new ManageImageApi(beans.getImageDelegate()));
beans.getWebServer().addPostApi("/admin/manageRepository", new ManageRepositoryApi(beans.getRepositoryDelegate()));
}
private void scheduleSync() {
beans.getTaskManager().scheduleRecurringTask(
new SynchroniseAllRepositoriesTask(beans.getSynchronisationDelegate()), beans.getProperties().getRefreshIntervalInMinutes(), TimeUnit.MINUTES
);
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.core;
import io.linuxserver.fleet.db.DefaultDatabaseConnection;
import io.linuxserver.fleet.db.dao.DefaultImageDAO;
import io.linuxserver.fleet.db.dao.DefaultRepositoryDAO;
import io.linuxserver.fleet.db.migration.DatabaseVersion;
import io.linuxserver.fleet.delegate.*;
import io.linuxserver.fleet.dockerhub.DockerHubV2Client;
import io.linuxserver.fleet.thread.TaskManager;
import io.linuxserver.fleet.web.WebServer;
/**
* <p>
* Initialises all relevant dependencies for the Fleet application
* </p>
*/
public class FleetBeans {
/**
* Contains all runtime properties that get loaded into the application on start-up
*/
private final FleetProperties properties;
/**
* Facade layer which handles delegation for image management.
*/
private final ImageDelegate imageDelegate;
private final RepositoryDelegate repositoryDelegate;
private final AuthenticationDelegate authenticationDelegate;
/**
* Facade layer for interaction with the Docker Hub APIs.
*/
private final DockerHubDelegate dockerHubDelegate;
private final SynchronisationDelegate synchronisationDelegate;
private final WebServer webServer;
private final TaskManager taskManager;
/**
* Ensures the database is kept up to date.
*/
private final DatabaseVersion databaseVersion;
FleetBeans() {
properties = new PropertiesLoader().getProperties();
final DefaultDatabaseConnection databaseConnection = new DefaultDatabaseConnection(properties);
databaseVersion = new DatabaseVersion(databaseConnection);
imageDelegate = new ImageDelegate(new DefaultImageDAO(databaseConnection));
repositoryDelegate = new RepositoryDelegate(new DefaultRepositoryDAO(databaseConnection));
dockerHubDelegate = new DockerHubDelegate(new DockerHubV2Client(properties.getDockerHubCredentials()));
authenticationDelegate = new PropertiesAuthenticationDelegate(properties.getAppUsername(), properties.getAppPassword());
synchronisationDelegate = new SynchronisationDelegate(imageDelegate, repositoryDelegate, dockerHubDelegate);
webServer = new WebServer(properties.getAppPort());
taskManager = new TaskManager();
}
public FleetProperties getProperties() {
return properties;
}
public ImageDelegate getImageDelegate() {
return imageDelegate;
}
public DockerHubDelegate getDockerHubDelegate() {
return dockerHubDelegate;
}
public RepositoryDelegate getRepositoryDelegate() {
return repositoryDelegate;
}
public SynchronisationDelegate getSynchronisationDelegate() {
return synchronisationDelegate;
}
public AuthenticationDelegate getAuthenticationDelegate() {
return authenticationDelegate;
}
public DatabaseVersion getDatabaseVersion() {
return databaseVersion;
}
public WebServer getWebServer() {
return webServer;
}
public TaskManager getTaskManager() {
return taskManager;
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.core;
import io.linuxserver.fleet.dockerhub.DockerHubCredentials;
import java.util.Properties;
public class FleetProperties {
private Properties properties;
FleetProperties(Properties properties) {
this.properties = properties;
}
public String getDatabaseDriverClassName() {
return properties.getProperty("fleet.database.driver");
}
public String getDatabaseUrl() {
return properties.getProperty("fleet.database.url");
}
public String getDatabaseUsername() {
return properties.getProperty("fleet.database.username");
}
public String getDatabasePassword() {
return properties.getProperty("fleet.database.password");
}
public String getAppUsername() {
return properties.getProperty("fleet.admin.username");
}
public String getAppPassword() {
return properties.getProperty("fleet.admin.password");
}
public int getAppPort() {
return Integer.parseInt(properties.getProperty("fleet.app.port"));
}
public int getRefreshIntervalInMinutes() {
return Integer.parseInt(properties.getProperty("fleet.refresh.interval"));
}
public DockerHubCredentials getDockerHubCredentials() {
String username = properties.getProperty("fleet.dockerhub.username");
String password = properties.getProperty("fleet.dockerhub.password");
return new DockerHubCredentials(username, password);
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.core;
public interface FleetRuntime {
/**
* Base directory for the config file.
*/
String CONFIG_BASE = System.getProperty("fleet.config.base");
/**
* Whether or not logs should show passwords
*/
boolean SHOW_PASSWORDS = System.getProperty("fleet.show.passwords") != null;
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.core;
public class Main {
public static void main(String[] args) {
FleetApp app = new FleetApp();
app.run();
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
/**
* <p>
* Loads in the application properties from disk. Properties must be provided as JVM arguments
* as these will be used to create a connection to the underlying database, and any other
* required connections.
* </p>
*
* @author Josh Stark
*/
public class PropertiesLoader extends BaseRuntimeLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesLoader.class);
private final FleetProperties properties;
PropertiesLoader() {
super();
try {
Properties properties = new Properties();
properties.load(new FileInputStream(FleetRuntime.CONFIG_BASE + "/fleet.properties"));
this.properties = new FleetProperties(properties);
printProperties();
if (!createStaticFileDirectory()) {
throw new RuntimeException("Unable to create the static files location for Fleet. Check permissions");
}
} catch (IOException e) {
LOGGER.error("Unable to load config! Check JVM args and config directory", e);
throw new RuntimeException(e);
}
}
private boolean createStaticFileDirectory() {
File staticFilesDir = new File(FleetRuntime.CONFIG_BASE + "/fleet_static");
if (staticFilesDir.exists()) {
return true;
}
return staticFilesDir.mkdir();
}
/**
* <p>
* The loaded properties, with accessible fields.
* </p>
*
* @return
* All application properties.
*/
protected FleetProperties getProperties() {
return properties;
}
/**
* <p>
* Prints out the loaded properties to the log. Useful when the application loads up for the first time.
* </p>
*/
private void printProperties() {
LOGGER.info("fleet.app.port : " + properties.getAppPort());
LOGGER.info("fleet.refresh.interval : " + properties.getRefreshIntervalInMinutes());
LOGGER.info("fleet.database.url : " + properties.getDatabaseUrl());
LOGGER.info("fleet.database.username : " + properties.getDatabaseUsername());
LOGGER.info("fleet.database.password : " + (showPasswords() ? properties.getDatabasePassword() : "***"));
LOGGER.info("fleet.dockerhub.username : " + properties.getDockerHubCredentials().getUsername());
LOGGER.info("fleet.dockerhub.password : " + (showPasswords() ? properties.getDockerHubCredentials().getPassword() : "***"));
}
/**
* <p>
* Ideally in a production environment you don't want to log passwords or keys.
* </p>
*
* @return
* true if passwords can be logged to the terminal.
*/
private boolean showPasswords() {
return FleetRuntime.SHOW_PASSWORDS;
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db;
import io.linuxserver.fleet.core.FleetProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DefaultDatabaseConnection extends PoolingDatabaseConnection {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDatabaseConnection.class);
public DefaultDatabaseConnection(FleetProperties properties) {
super(properties);
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db;
import com.zaxxer.hikari.HikariDataSource;
import io.linuxserver.fleet.core.FleetProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
public abstract class PoolingDatabaseConnection {
private static final Logger LOGGER = LoggerFactory.getLogger(PoolingDatabaseConnection.class);
private final HikariDataSource dataSource;
PoolingDatabaseConnection(FleetProperties properties) {
dataSource = new HikariDataSource();
dataSource.setDriverClassName(properties.getDatabaseDriverClassName());
dataSource.setJdbcUrl(properties.getDatabaseUrl());
dataSource.setUsername(properties.getDatabaseUsername());
dataSource.setPassword(properties.getDatabasePassword());
LOGGER.info("DataSource established: " + dataSource.getJdbcUrl());
}
public DataSource getDataSource() {
return dataSource;
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}

View File

@ -0,0 +1,178 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.dao;
import io.linuxserver.fleet.db.PoolingDatabaseConnection;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.db.query.LimitedResult;
import io.linuxserver.fleet.model.Image;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import static io.linuxserver.fleet.db.dao.Utils.setNullableInt;
import static io.linuxserver.fleet.db.dao.Utils.setNullableLong;
public class DefaultImageDAO implements ImageDAO {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultImageDAO.class);
private final PoolingDatabaseConnection databaseConnection;
public DefaultImageDAO(PoolingDatabaseConnection databaseConnection) {
this.databaseConnection = databaseConnection;
}
@Override
public Image findImageByRepositoryAndImageName(int repositoryId, String imageName) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Image_GetByName(?,?)}");
call.setInt(1, repositoryId);
call.setString(2, imageName);
ResultSet results = call.executeQuery();
if (results.next())
return parseImageFromResultSet(results);
} catch (SQLException e) {
LOGGER.error("Unable to fetch image", e);
}
return null;
}
@Override
public Image fetchImage(Integer id) {
LOGGER.debug("Fetching image by ID: " + id);
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Image_Get(?)}");
call.setInt(1, id);
ResultSet results = call.executeQuery();
if (results.next())
return parseImageFromResultSet(results);
} catch (SQLException e) {
LOGGER.error("Unable to fetch image.", e);
}
return null;
}
@Override
public LimitedResult<Image> fetchImagesByRepository(int repositoryId) {
List<Image> images = new ArrayList<>();
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Image_GetAll(?,?)}");
call.setInt(1, repositoryId);
call.registerOutParameter(2, Types.INTEGER);
ResultSet results = call.executeQuery();
while (results.next())
images.add(parseImageFromResultSet(results));
int totalRecords = call.getInt(2);
return new LimitedResult<>(images, totalRecords);
} catch (SQLException e) {
LOGGER.error("Unable to get all images", e);
}
return new LimitedResult<>(images, images.size());
}
@Override
public InsertUpdateResult<Image> saveImage(Image image) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Image_Save(?,?,?,?,?,?,?,?,?,?)");
setNullableInt(call, 1, image.getId());
call.setInt(2, image.getRepositoryId());
call.setString(3, image.getName());
setNullableLong(call, 4, image.getPullCount());
call.setString(5, image.getVersion());
call.setBoolean(6, image.isHidden());
call.setBoolean(7, image.isUnstable());
call.registerOutParameter(8, Types.INTEGER);
call.registerOutParameter(9, Types.INTEGER);
call.registerOutParameter(10, Types.VARCHAR);
call.executeUpdate();
int imageId = call.getInt(8);
int status = call.getInt(9);
String statusMessage = call.getString(10);
if (InsertUpdateStatus.OK == status)
return new InsertUpdateResult<>(fetchImage(imageId), status, statusMessage);
return new InsertUpdateResult<>(status, statusMessage);
} catch (SQLException e) {
LOGGER.error("Unable to save image", e);
return new InsertUpdateResult<>(null, InsertUpdateStatus.OK, "Unable to save image");
}
}
@Override
public void removeImage(Integer id) {
try (Connection connection = databaseConnection.getConnection()) {
PreparedStatement call = connection.prepareStatement("DELETE FROM Images WHERE `id` = ?");
call.setInt(1, id);
call.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Error when removing image", e);
}
}
private Image parseImageFromResultSet(ResultSet results) throws SQLException {
Image image = new Image(
results.getInt("ImageId"),
results.getInt("RepositoryId"),
results.getString("ImageName")
);
return image
.withVersion(results.getString("ImageVersion"))
.withPullCount(results.getLong("ImagePullCount"))
.withVersionMask(results.getString("ImageVersionMask"))
.withModifiedTime(results.getTimestamp("ModifiedTime").toLocalDateTime())
.withHidden(results.getBoolean("ImageHidden"))
.withUnstable(results.getBoolean("ImageUnstable"));
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.dao;
import io.linuxserver.fleet.db.PoolingDatabaseConnection;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.model.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import static io.linuxserver.fleet.db.dao.Utils.setNullableInt;
import static io.linuxserver.fleet.db.dao.Utils.setNullableString;
public class DefaultRepositoryDAO implements RepositoryDAO {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultImageDAO.class);
private final PoolingDatabaseConnection databaseConnection;
public DefaultRepositoryDAO(PoolingDatabaseConnection databaseConnection) {
this.databaseConnection = databaseConnection;
}
@Override
public Repository fetchRepository(int id) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_Get(?)}");
call.setInt(1, id);
ResultSet results = call.executeQuery();
if (results.next())
return parseRepositoryFromResultSet(results);
} catch (SQLException e) {
LOGGER.error("Unable to retrieve repository", e);
}
return null;
}
@Override
public InsertUpdateResult<Repository> saveRepository(Repository repository) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_Save(?,?,?,?,?,?,?)}");
setNullableInt(call, 1, repository.getId());
call.setString(2, repository.getName());
setNullableString(call, 3, repository.getVersionMask());
call.setBoolean(4, repository.isSyncEnabled());
call.registerOutParameter(5, Types.INTEGER);
call.registerOutParameter(6, Types.INTEGER);
call.registerOutParameter(7, Types.VARCHAR);
call.executeUpdate();
int repositoryId = call.getInt(5);
int status = call.getInt(6);
String statusMessage = call.getString(7);
if (InsertUpdateStatus.OK == status)
return new InsertUpdateResult<>(fetchRepository(repositoryId), status, statusMessage);
return new InsertUpdateResult<>(status, statusMessage);
} catch (SQLException e) {
LOGGER.error("Unable to save repository", e);
return new InsertUpdateResult<>(null, InsertUpdateStatus.OK, "Unable to save repository");
}
}
@Override
public List<Repository> fetchAllRepositories() {
List<Repository> repositories = new ArrayList<>();
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_GetAll()}");
ResultSet results = call.executeQuery();
while (results.next())
repositories.add(parseRepositoryFromResultSet(results));
} catch (SQLException e) {
LOGGER.error("Unable to get all repositories", e);
}
return repositories;
}
@Override
public Repository findRepositoryByName(String name) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_GetByName(?)}");
call.setString(1, name);
ResultSet results = call.executeQuery();
if (results.next())
return parseRepositoryFromResultSet(results);
} catch (SQLException e) {
LOGGER.error("Unable to retrieve repository", e);
}
return null;
}
@Override
public void removeRepository(int id) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_Delete(?)}");
call.setInt(1, id);
call.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Error when removing repository", e);
}
}
private Repository parseRepositoryFromResultSet(ResultSet results) throws SQLException {
Repository repository = new Repository(results.getInt("RepositoryId"), results.getString("RepositoryName"))
.withSyncEnabled(results.getBoolean("SyncEnabled"))
.withVersionMask(results.getString("RepositoryVersionMask"))
.withModifiedTime(results.getTimestamp("ModifiedTime").toLocalDateTime());
LOGGER.debug("Parsed repository: " + repository);
return repository;
}
}

View File

@ -0,0 +1,18 @@
package io.linuxserver.fleet.db.dao;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.LimitedResult;
import io.linuxserver.fleet.model.Image;
public interface ImageDAO {
Image findImageByRepositoryAndImageName(int repositoryId, String imageName);
Image fetchImage(Integer id);
LimitedResult<Image> fetchImagesByRepository(int repositoryId);
InsertUpdateResult<Image> saveImage(Image image);
void removeImage(Integer id);
}

View File

@ -0,0 +1,19 @@
package io.linuxserver.fleet.db.dao;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.model.Repository;
import java.util.List;
public interface RepositoryDAO {
Repository fetchRepository(int id);
InsertUpdateResult<Repository> saveRepository(Repository repository);
List<Repository> fetchAllRepositories();
Repository findRepositoryByName(String name);
void removeRepository(int id);
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.dao;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.sql.Types;
class Utils {
static void setNullableInt(CallableStatement call, int position, Integer value) throws SQLException {
if (null == value)
call.setNull(position, Types.INTEGER);
else
call.setInt(position, value);
}
static void setNullableLong(CallableStatement call, int position, Long value) throws SQLException {
if (null == value)
call.setNull(position, Types.BIGINT);
else
call.setLong(position, value);
}
static void setNullableString(CallableStatement call, int position, String value) throws SQLException {
if (null == value)
call.setNull(position, Types.VARCHAR);
else
call.setString(position, value);
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.migration;
import io.linuxserver.fleet.db.PoolingDatabaseConnection;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.FlywayException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* Manages versioning of the database to ensure it is kept up-to-date
* </p>
*/
public class DatabaseVersion {
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseVersion.class);
private final Flyway flyway;
public DatabaseVersion(PoolingDatabaseConnection databaseConnection) {
flyway = Flyway.configure().dataSource(databaseConnection.getDataSource()).load();
}
/**
* <p>
* Runs the migration process which runs any necessary scripts to get the database configured.
* </p>
*/
public void migrate() {
try {
flyway.migrate();
} catch (FlywayException e) {
LOGGER.error(e.getMessage());
throw new RuntimeException("Unable to start application because the database has gone out of sync.");
}
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.query;
public class InsertUpdateResult<T> {
private final T result;
private final int status;
private final String statusMessage;
public InsertUpdateResult(T result, int status, String statusMessage) {
this.result = result;
this.status = status;
this.statusMessage = statusMessage;
}
public InsertUpdateResult(int status, String statusMessage) {
this(null, status, statusMessage);
}
public T getResult() {
return result;
}
public int getStatus() {
return status;
}
public String getStatusMessage() {
return statusMessage;
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.query;
public interface InsertUpdateStatus {
int OK = 0;
int FAILED = 1;
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.query;
public class LimitOffset {
private final int limit;
private final int offset;
public LimitOffset(int limit, int offset) {
this.limit = limit;
this.offset = offset;
}
public int getLimit() {
return limit;
}
public int getOffset() {
return offset;
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.query;
import java.util.List;
public class LimitedResult<T> {
private final List<T> results;
private final LimitOffset next;
private final int totalCount;
public LimitedResult(List<T> results, int totalCount) {
this(results, totalCount,null);
}
public LimitedResult(List<T> results, int totalCount, LimitOffset next) {
this.results = results;
this.totalCount = totalCount;
this.next = next;
}
public List<T> getResults() {
return results;
}
public int getTotalCount() {
return totalCount;
}
public LimitOffset getNext() {
return next;
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
import io.linuxserver.fleet.auth.AuthenticationResult;
public interface AuthenticationDelegate {
AuthenticationResult authenticate(String username, String password);
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
import io.linuxserver.fleet.dockerhub.DockerHubClient;
import io.linuxserver.fleet.dockerhub.model.DockerHubV2Image;
import io.linuxserver.fleet.model.DockerHubImage;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class DockerHubDelegate {
private final DockerHubClient dockerHubClient;
public DockerHubDelegate(DockerHubClient dockerHubClient) {
this.dockerHubClient = dockerHubClient;
}
public List<String> fetchAllRepositories() {
return dockerHubClient.fetchAllRepositories().getNamespaces();
}
public List<DockerHubImage> fetchAllImagesFromRepository(String repositoryName) {
List<DockerHubImage> images = new ArrayList<>();
for (DockerHubV2Image apiImage : dockerHubClient.fetchImagesFromRepository(repositoryName))
images.add(convertApiImageToInternalImage(apiImage));
images.sort(Comparator.comparing(DockerHubImage::getName));
return images;
}
public String fetchLatestImageTag(String repositoryName, String imageName) {
return dockerHubClient.fetchLatestTagForImage(repositoryName, imageName).getName();
}
private DockerHubImage convertApiImageToInternalImage(DockerHubV2Image apiImage) {
return new DockerHubImage(
apiImage.getName(),
apiImage.getNamespace(),
apiImage.getDescription(),
apiImage.getStarCount(),
apiImage.getPullCount(),
parseDockerHubDate(apiImage.getLastUpdated())
);
}
private LocalDateTime parseDockerHubDate(String date) {
if (null == date)
return null;
return LocalDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"));
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
import io.linuxserver.fleet.db.dao.ImageDAO;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.exception.SaveException;
import io.linuxserver.fleet.model.Image;
import java.util.List;
public class ImageDelegate {
private final ImageDAO imageDAO;
public ImageDelegate(ImageDAO imageDAO) {
this.imageDAO = imageDAO;
}
/**
* <p>
* Performs a look-up to see if an image exists in the persisted database which matches
* the given repository and image name. This is used to determine if, when scanning, Fleet
* already has the image stored against the repository.
* </p>
*/
public Image findImageByRepositoryAndImageName(int repositoryId, String imageName) {
return imageDAO.findImageByRepositoryAndImageName(repositoryId, imageName);
}
public Image fetchImage(int id) {
return imageDAO.fetchImage(id);
}
public List<Image> fetchImagesByRepository(int repositoryId) {
return imageDAO.fetchImagesByRepository(repositoryId).getResults();
}
public void removeImage(Integer id) {
imageDAO.removeImage(id);
}
public Image saveImage(Image image) throws SaveException {
InsertUpdateResult<Image> result = imageDAO.saveImage(image);
if (result.getStatus() == InsertUpdateStatus.OK)
return result.getResult();
throw new SaveException(result.getStatusMessage());
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
import io.linuxserver.fleet.auth.AuthenticatedUser;
import io.linuxserver.fleet.auth.AuthenticationResult;
public class PropertiesAuthenticationDelegate implements AuthenticationDelegate {
private final String adminUsername;
private final String adminPassword;
public PropertiesAuthenticationDelegate(String username, String password) {
this.adminUsername = username;
this.adminPassword = password;
}
@Override
public AuthenticationResult authenticate(String username, String password) {
if (adminUsername.equals(username) && adminPassword.equals(password))
return new AuthenticationResult(true, new AuthenticatedUser(username));
return AuthenticationResult.notAuthenticated();
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
import io.linuxserver.fleet.db.dao.RepositoryDAO;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.exception.SaveException;
import io.linuxserver.fleet.model.Repository;
import java.util.List;
public class RepositoryDelegate {
private final RepositoryDAO repositoryDAO;
public RepositoryDelegate(RepositoryDAO repositoryDAO) {
this.repositoryDAO = repositoryDAO;
}
public Repository fetchRepository(int id) {
return repositoryDAO.fetchRepository(id);
}
public Repository saveRepository(Repository repository) throws SaveException {
InsertUpdateResult<Repository> result = repositoryDAO.saveRepository(repository);
if (result.getStatus() == InsertUpdateStatus.OK)
return result.getResult();
throw new SaveException(result.getStatusMessage());
}
public List<Repository> fetchAllRepositories() {
return repositoryDAO.fetchAllRepositories();
}
public Repository findRepositoryByName(String name) {
return repositoryDAO.findRepositoryByName(name);
}
public void removeRepository(int id) {
repositoryDAO.removeRepository(id);
}
}

View File

@ -0,0 +1,205 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
import io.linuxserver.fleet.exception.SaveException;
import io.linuxserver.fleet.model.DockerHubImage;
import io.linuxserver.fleet.model.Image;
import io.linuxserver.fleet.model.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>
* Handles the one-way synchronisation of Docker Hub repositories and images over to the Fleet
* database. Any newly created image in Docker Hub will be automatically picked up and stored, but any
* new repositories will be marked as skipped until someone manually sets it to be synchronised.
* </p>
*/
public class SynchronisationDelegate {
private static final Logger LOGGER = LoggerFactory.getLogger(SynchronisationDelegate.class);
private final ImageDelegate imageDelegate;
private final RepositoryDelegate repositoryDelegate;
private final DockerHubDelegate dockerHubDelegate;
public SynchronisationDelegate(ImageDelegate imageDelegate, RepositoryDelegate repositoryDelegate, DockerHubDelegate dockerHubDelegate) {
this.imageDelegate = imageDelegate;
this.repositoryDelegate = repositoryDelegate;
this.dockerHubDelegate = dockerHubDelegate;
}
public void synchronise() {
synchronise((message) -> LOGGER.info(message.toString()));
}
/**
* <p>
* Scans Docker Hub for all images against all registered repositories for the user. It will then update the pull
* and version information for each.
* </p>
*/
public void synchronise(SynchronisationListener listener) {
if (null == listener) {
synchronise();
return;
}
LOGGER.info("Starting synchronisation of all repositories");
List<String> repositories = dockerHubDelegate.fetchAllRepositories();
for (String repositoryName : repositories)
synchroniseRepository(repositoryName, listener);
LOGGER.info("Synchronisation complete");
}
private void synchroniseRepository(String repositoryName, SynchronisationListener listener) {
Repository repository = configureRepository(repositoryName);
if (repository.isSyncEnabled()) {
listener.onEvent("Synchronising " + repository);
List<DockerHubImage> images = dockerHubDelegate.fetchAllImagesFromRepository(repository.getName());
LOGGER.info("Found " + images.size() + " images in Docker Hub");
for (DockerHubImage dockerHubImage : images)
synchroniseImage(repository, dockerHubImage, listener);
} else {
listener.onEvent("Skipping sync for " + repositoryName);
}
}
private void synchroniseImage(Repository repository, DockerHubImage dockerHubImage, SynchronisationListener listener) {
try {
Image image = configureImage(repository.getId(), dockerHubImage);
String maskedVersion = getMaskedVersion(repository.getName(), image.getName(), image.getVersionMask());
LOGGER.debug("Updated image version using mask. Mask=" + image.getVersionMask() + ", MaskedVersion=" + maskedVersion);
image.withPullCount(dockerHubImage.getPullCount()).withVersion(maskedVersion);
listener.onEvent("Synchronising " + image);
imageDelegate.saveImage(image);
} catch (SaveException e) {
LOGGER.error("Unable to save updated image", e);
}
}
private String getMaskedVersion(String repositoryName, String imageName, String versionMask) {
String tag = dockerHubDelegate.fetchLatestImageTag(repositoryName, imageName);
if (isTagJustLatestAndNotAVersion(tag) || null == versionMask)
return tag;
return extractMaskedVersion(tag, versionMask);
}
/**
* Given the repository name from Docker Hub, this will attempt to find it in the database. If it exists, it gets returned,
* otherwise a new record is created (with sync disabled) and that new record is returned instead.
*/
private Repository configureRepository(String repositoryName) {
Repository repository = repositoryDelegate.findRepositoryByName(repositoryName);
if (isRepositoryNew(repository)) {
try {
return repositoryDelegate.saveRepository(new Repository(repositoryName));
} catch (SaveException e) {
LOGGER.error("Tried to save new repository during sync but failed", e);
}
}
return repository;
}
/**
* Looks up the image in the database to see if it already exists. If it does, it gets returned, otherwise a base
* image is created with just the top-level information, as the rest will get updated later.
*/
private Image configureImage(int repositoryId, DockerHubImage dockerHubImage) {
Image image = imageDelegate.findImageByRepositoryAndImageName(repositoryId, dockerHubImage.getName());
if (isImageNew(image)) {
try {
return imageDelegate.saveImage(new Image(repositoryId, dockerHubImage.getName()));
} catch (SaveException e) {
LOGGER.error("Tried to save new image during sync but failed", e);
}
}
return image;
}
private String extractMaskedVersion(String fullTag, String versionMask) {
Pattern pattern = Pattern.compile(versionMask);
Matcher matcher = pattern.matcher(fullTag);
if (matcher.matches()) {
StringBuilder tagBuilder = new StringBuilder();
for (int groupNum = 1; groupNum <= matcher.groupCount(); groupNum++)
tagBuilder.append(matcher.group(groupNum));
return tagBuilder.toString();
}
return fullTag;
}
/**
* <p>
* If the top-level tag is not versioned, a mask can't be applied.
* </p>
*/
private boolean isTagJustLatestAndNotAVersion(String tag) {
return "latest".equals(tag);
}
private boolean isRepositoryNew(Repository repository) {
return null == repository;
}
private boolean isImageNew(Image image) {
return null == image;
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
public interface SynchronisationListener {
void onEvent(Object eventMessage);
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub;
import io.linuxserver.fleet.rest.RestClient;
import io.linuxserver.fleet.rest.RestResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class DockerHubAuthenticator {
private static final Logger LOGGER = LoggerFactory.getLogger(DockerHubAuthenticator.class);
private final RestClient client;
private final DockerHubCredentials credentials;
private String token;
DockerHubAuthenticator(DockerHubCredentials credentials, RestClient client) {
this.credentials = credentials;
this.client = client;
refreshToken();
}
/**
* <p>
* Re-authenticates with Docker Hub to obtain a fresh JWT.
* </p>
*
* @return
* The new JWT to be used in authenticated requests.
*/
synchronized String refreshToken() {
LOGGER.info("Refreshing token for Docker Hub authentication");
RestResponse<DockerHubTokenResponse> authenticationResponse = client.executePost(
DockerHubV2Client.DOCKERHUB_BASE_URI + "/users/login", null, null, credentials, DockerHubTokenResponse.class);
if (authenticationResponse.getStatusCode() == 200) {
LOGGER.info("Refresh successful");
String token = authenticationResponse.getPayload().getToken();
this.token = token;
return token;
}
LOGGER.info("Unable to refresh token.");
throw new RuntimeException("Unable to authenticate with Docker Hub. Check credentials");
}
synchronized String getCurrentToken() {
if (null == token)
return refreshToken();
return token;
}
static class DockerHubTokenResponse {
private String token;
public String getToken() {
return token;
}
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub;
import io.linuxserver.fleet.dockerhub.model.DockerHubV2Image;
import io.linuxserver.fleet.dockerhub.model.DockerHubV2NamespaceLookupResult;
import io.linuxserver.fleet.dockerhub.model.DockerHubV2Tag;
import java.util.List;
public interface DockerHubClient {
DockerHubV2NamespaceLookupResult fetchAllRepositories();
List<DockerHubV2Image> fetchImagesFromRepository(String repositoryName);
DockerHubV2Image fetchImageFromRepository(String repositoryName, String imageName);
DockerHubV2Tag fetchLatestTagForImage(String repositoryName, String imageName);
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub;
public class DockerHubCredentials {
private final String username;
private final String password;
public DockerHubCredentials(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub;
public class DockerHubException extends RuntimeException {
public DockerHubException(String message) {
super(message);
}
public DockerHubException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,159 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub;
import io.linuxserver.fleet.dockerhub.model.*;
import io.linuxserver.fleet.rest.HttpException;
import io.linuxserver.fleet.rest.RestClient;
import io.linuxserver.fleet.rest.RestResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Client class to interface with Docker Hub's V2 API.
*/
public class DockerHubV2Client implements DockerHubClient {
static final String DOCKERHUB_BASE_URI = "https://hub.docker.com/v2";
private final RestClient restClient;
private final DockerHubAuthenticator authenticator;
public DockerHubV2Client(DockerHubCredentials credentials) {
this.restClient = new RestClient();
this.authenticator = new DockerHubAuthenticator(credentials, restClient);
}
@Override
public DockerHubV2NamespaceLookupResult fetchAllRepositories() {
try {
String url = DOCKERHUB_BASE_URI + "/repositories/namespaces";
RestResponse<DockerHubV2NamespaceLookupResult> response = doCall(url, DockerHubV2NamespaceLookupResult.class);
if (isResponseOK(response)) {
return response.getPayload();
}
return new DockerHubV2NamespaceLookupResult();
} catch (HttpException e) {
throw new DockerHubException("Unable to get repositories", e);
}
}
@Override
public List<DockerHubV2Image> fetchImagesFromRepository(String repositoryName) {
List<DockerHubV2Image> images = new ArrayList<>();
try {
String url = DOCKERHUB_BASE_URI + "/repositories/" + repositoryName;
while (url != null) {
RestResponse<DockerHubV2ImageListResult> response = doCall(url, DockerHubV2ImageListResult.class);
if (isResponseOK(response)) {
DockerHubV2ImageListResult payload = response.getPayload();
images.addAll(payload.getResults());
url = payload.getNext();
}
}
return images;
} catch (HttpException e) {
throw new DockerHubException("Unable to get images for " + repositoryName, e);
}
}
@Override
public DockerHubV2Image fetchImageFromRepository(String repositoryName, String imageName) {
try {
String absoluteUrl = DOCKERHUB_BASE_URI + "/repositories/" + repositoryName + "/" + imageName;
RestResponse<DockerHubV2Image> restResponse = doCall(absoluteUrl, DockerHubV2Image.class);
return restResponse.getPayload();
} catch (HttpException e) {
throw new DockerHubException("Unable to get images for " + repositoryName + "/" + imageName, e);
}
}
@Override
public DockerHubV2Tag fetchLatestTagForImage(String repositoryName, String imageName) {
try {
String absoluteUrl = DOCKERHUB_BASE_URI + "/repositories/" + repositoryName + "/" + imageName + "/tags" ;
RestResponse<DockerHubV2TagListResult> restResponse = doCall(absoluteUrl, DockerHubV2TagListResult.class);
return restResponse.getPayload().getResults().get(0);
} catch (HttpException e) {
throw new DockerHubException("Unable to get tags for " + repositoryName + "/" + imageName, e);
}
}
/**
* <p>
* Attempts to call DockerHub with the currently set credentials. If they have expired, it will refresh them and try again.
* This process will only try again once, so if the refresh resulted in another stale token, it will need to be handled.
* </p>
*/
private <T> RestResponse<T> doCall(String url, Class<T> responseType) {
RestResponse<T> restResponse = restClient.executeGet(url, null, buildHeaders(), responseType);
if (isResponseUnauthorised(restResponse)) {
authenticator.refreshToken();
restResponse = restClient.executeGet(url, null, buildHeaders(), responseType);
}
return restResponse;
}
private Map<String, String> buildHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "JWT " + authenticator.getCurrentToken());
return headers;
}
private boolean isResponseOK(RestResponse restResponse) {
return restResponse.getStatusCode() == 200;
}
private boolean isResponseUnauthorised(RestResponse restResponse) {
return restResponse.getStatusCode() == 401;
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class DockerHubV2Image {
@JsonProperty("user")
private String user;
@JsonProperty("name")
private String name;
@JsonProperty("namespace")
private String namespace;
@JsonProperty("description")
private String description;
@JsonProperty("star_count")
private int starCount;
@JsonProperty("pull_count")
private long pullCount;
@JsonProperty("last_updated")
private String lastUpdated;
public final String getUser() {
return user;
}
public final String getName() {
return name;
}
public final String getNamespace() {
return namespace;
}
public String getDescription() {
return description;
}
public final int getStarCount() {
return starCount;
}
public final long getPullCount() {
return pullCount;
}
public final String getLastUpdated() {
return lastUpdated;
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub.model;
public class DockerHubV2ImageListResult extends DockerHubV2ScanResult<DockerHubV2Image> {
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
public class DockerHubV2NamespaceLookupResult {
@JsonProperty("namespaces")
private List<String> namespaces = new ArrayList<>();
public List<String> getNamespaces() {
return namespaces;
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class DockerHubV2ScanResult<T> {
@JsonProperty("count")
private int count;
@JsonProperty("next")
private String next;
@JsonProperty("previous")
private String previous;
@JsonProperty("results")
private List<T> results;
public final int getCount() {
return count;
}
public final String getNext() {
return next;
}
public final String getPrevious() {
return previous;
}
public final List<T> getResults() {
return results;
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DockerHubV2Tag {
@JsonProperty("name")
private String name;
@JsonProperty("full_size")
private long fullSize;
public String getName() {
return name;
}
public long getFullSize() {
return fullSize;
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.dockerhub.model;
public class DockerHubV2TagListResult extends DockerHubV2ScanResult<DockerHubV2Tag> {
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.exception;
public class SaveException extends Exception {
public SaveException(String message) {
super(message);
}
public SaveException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model;
import java.time.LocalDateTime;
/**
* <p>
* Fleet's specifically required structure for an image coming from DockerHub. This object
* is common within the app, and is immune to any API changes to DockerHub itself.
* </p>
*/
public class DockerHubImage {
private final String name;
private final String repository;
private final String description;
private final int starCount;
private final long pullCount;
private final LocalDateTime buildDate;
public DockerHubImage(String name, String repository, String description, int starCount, long pullCount, LocalDateTime buildDate) {
this.name = name;
this.repository = repository;
this.description = description;
this.starCount = starCount;
this.pullCount = pullCount;
this.buildDate = buildDate;
}
public String getName() {
return name;
}
public String getRepository() {
return repository;
}
public String getDescription() {
return description;
}
public int getStarCount() {
return starCount;
}
public long getPullCount() {
return pullCount;
}
public LocalDateTime getBuildDate() {
return buildDate;
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model;
/**
* <p>
* Representation of a stored image in the Fleet database. Each image contains
* specific information regarding its build status and pull count.
* </p>
*/
public class Image extends PersistableItem<Image> {
private final int repositoryId;
private final String name;
private String version;
private long pullCount;
private String versionMask;
private boolean unstable;
private boolean hidden;
public Image(Integer id, int repositoryId, String name) {
super(id);
this.name = name;
this.repositoryId = repositoryId;
}
public Image(int repositoryId, String name) {
this(null, repositoryId, name);
}
public Image withVersion(String version) {
this.version = version;
return this;
}
public Image withPullCount(long pullCount) {
this.pullCount = pullCount;
return this;
}
public Image withVersionMask(String versionMask) {
this.versionMask = versionMask;
return this;
}
public Image withHidden(boolean hidden) {
this.hidden = hidden;
return this;
}
public Image withUnstable(boolean unstable) {
this.unstable = unstable;
return this;
}
public int getRepositoryId() {
return repositoryId;
}
public String getName() {
return name;
}
public long getPullCount() {
return pullCount;
}
public String getVersion() {
return version;
}
public String getVersionMask() {
return versionMask;
}
public boolean isUnstable() {
return unstable;
}
public boolean isHidden() {
return hidden;
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.time.LocalDateTime;
public abstract class PersistableItem<T extends PersistableItem> {
private Integer id;
private LocalDateTime modifiedTime;
PersistableItem() { }
PersistableItem(Integer id) {
this.id = id;
}
@SuppressWarnings("unchecked")
public T withId(Integer id) {
if (null == id)
throw new IllegalArgumentException("Item ID must not be null");
this.id = id;
return (T) this;
}
@SuppressWarnings("unchecked")
public T withModifiedTime(LocalDateTime modifiedTime) {
this.modifiedTime = modifiedTime;
return (T) this;
}
public Integer getId() {
return id;
}
public LocalDateTime getModifiedTime() {
return modifiedTime;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model;
public class Repository extends PersistableItem<Repository> {
private final String name;
private String versionMask;
private boolean syncEnabled;
public Repository(Integer id, String name) {
super(id);
this.name = name;
}
public Repository(String name) {
super();
this.name = name;
}
public Repository withVersionMask(String versionMask) {
this.versionMask = versionMask;
return this;
}
public Repository withSyncEnabled(boolean syncEnabled) {
this.syncEnabled = syncEnabled;
return this;
}
public String getName() {
return name;
}
public String getVersionMask() {
return versionMask;
}
public boolean isSyncEnabled() {
return syncEnabled;
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model;
import java.util.Collections;
import java.util.List;
public class RepositoryWithImages {
private final Repository repository;
private final List<Image> images;
public RepositoryWithImages(Repository repository, List<Image> images) {
this.repository = repository;
this.images = Collections.unmodifiableList(images);
}
public Repository getRepository() {
return repository;
}
public List<Image> getImages() {
return images;
}
public boolean isEveryImageStable() {
return images.stream().noneMatch(Image::isUnstable);
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.api;
import io.linuxserver.fleet.model.Image;
public class ApiImage {
private String name;
private long pullCount;
private String version;
private boolean stable;
public String getName() {
return name;
}
public long getPullCount() {
return pullCount;
}
public String getVersion() {
return version;
}
public boolean isStable() {
return stable;
}
public static ApiImage fromImage(Image image) {
ApiImage apiImage = new ApiImage();
apiImage.name = image.getName();
apiImage.pullCount = image.getPullCount();
apiImage.version = image.getVersion();
apiImage.stable = !image.isUnstable();
return apiImage;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.api;
import java.util.List;
import java.util.Map;
public class ApiImagesWithTotalCount {
private final long totalPullCount;
private final Map<String, List<ApiImage>> repositories;
public ApiImagesWithTotalCount(long totalPullCount, Map<String, List<ApiImage>> repositories) {
this.totalPullCount = totalPullCount;
this.repositories = repositories;
}
public long getTotalPullCount() {
return totalPullCount;
}
public Map<String, List<ApiImage>> getRepositories() {
return repositories;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.api;
public class ApiResponse<T> {
private final String status;
private final T data;
public ApiResponse(String status, T data) {
this.status = status;
this.data = data;
}
public String getStatus() {
return status;
}
public T getData() {
return data;
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.api;
public class FleetApiException extends RuntimeException {
private final int statusCode;
public FleetApiException(int statusCode, String message) {
super(message);
this.statusCode = statusCode;
}
public FleetApiException(int statusCode, String message, Throwable cause) {
super(message, cause);
this.statusCode = statusCode;
}
public int getStatusCode() {
return statusCode;
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.rest;
public class HttpException extends RuntimeException {
public HttpException(String message) {
super(message);
}
public HttpException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,154 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.rest;
import io.linuxserver.fleet.rest.marshalling.JacksonMarshallingStrategy;
import io.linuxserver.fleet.rest.marshalling.MarshallingStrategy;
import io.linuxserver.fleet.rest.proxy.LazyLoadPayloadProxy;
import org.apache.http.HttpEntity;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Map;
/**
* <p>
* Simple wrapper for base HttpClient, providing an easier way to marshall/unmarshall payloads
* </p>
*/
public class RestClient {
private static final Logger LOGGER = LoggerFactory.getLogger(RestClient.class);
private final CloseableHttpClient client;
private final HttpClientContext clientContext;
private MarshallingStrategy marshallingStrategy;
public RestClient() {
setMarshallingStrategy(new JacksonMarshallingStrategy());
client = HttpClients.custom().setConnectionManager(new PoolingHttpClientConnectionManager()).build();
clientContext = HttpClientContext.create();
}
private void setMarshallingStrategy(MarshallingStrategy marshallingStrategy) {
LOGGER.debug("Configuring RestClient with " + marshallingStrategy.getClass().getName());
this.marshallingStrategy = marshallingStrategy;
}
public <T> RestResponse<T> executeGet(String url, Map<String, String> queryParameters, Map<String, String> headers, Class<T> responseType) {
try {
return executeBaseRequest(responseType, headers, new HttpGet(url + parseQueryParameters(queryParameters)));
} catch (IOException e) {
LOGGER.error("Unable to perform GET", e);
throw new HttpException("Unable to perform GET", e);
}
}
public <T> RestResponse<T> executePost(String url, Map<String, String> queryParameters, Map<String, String> headers, Object payload, Class<T> responseType) {
try {
HttpPost post = new HttpPost(url + parseQueryParameters(queryParameters));
post.setEntity(new StringEntity(marshallingStrategy.marshall(payload), Charset.forName("UTF-8")));
post.setHeader("Content-Type", marshallingStrategy.getContentType());
return executeBaseRequest(responseType, headers, post);
} catch (IOException e) {
LOGGER.error("Unable to perform GET", e);
throw new HttpException("Unable to perform GET", e);
}
}
private <T> RestResponse<T> executeBaseRequest(Class<T> responseType, Map<String, String> headers, HttpRequestBase request) throws IOException {
LOGGER.debug("url : " + request.getURI().toString());
LOGGER.debug("headers : " + headers);
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet())
request.setHeader(header.getKey(), header.getValue());
}
LOGGER.debug("Executing.");
try (CloseableHttpResponse response = client.execute(request, clientContext)) {
StatusLine statusLine = response.getStatusLine();
LOGGER.debug("Response status: " + statusLine);
int statusCode = statusLine.getStatusCode();
HttpEntity content = response.getEntity();
String responseBody = null;
if (null != content) {
responseBody = EntityUtils.toString(content);
LOGGER.debug("Parsed response payload: " + responseBody);
}
if (null != responseBody)
return new RestResponse<>(new LazyLoadPayloadProxy<>(marshallingStrategy, responseBody, responseType), statusCode);
return new RestResponse<>(statusCode);
} finally {
request.releaseConnection();
}
}
private String parseQueryParameters(Map<String, String> queryParameters) {
if (null != queryParameters) {
StringBuilder builtParameterString = new StringBuilder("?");
for (Map.Entry<String, String> param : queryParameters.entrySet())
builtParameterString.append(param.getKey()).append("=").append(param.getValue()).append("&");
builtParameterString.setLength(builtParameterString.length() - 1);
return builtParameterString.toString();
}
return "";
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.rest;
import io.linuxserver.fleet.rest.proxy.PayloadProxy;
public class RestResponse<T> {
private PayloadProxy<T> payloadProxy;
private int statusCode;
RestResponse(int statusCode) {
this(null, statusCode);
}
RestResponse(PayloadProxy<T> payloadProxy, int statusCode) {
this.payloadProxy = payloadProxy;
this.statusCode = statusCode;
}
public T getPayload() {
return payloadProxy.get();
}
public int getStatusCode() {
return statusCode;
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.rest.marshalling;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
/**
* Jackson JSON implementation of the marshalling strategy. This will convert incoming
* and outgoing messages formatted in JSON.
*/
public class JacksonMarshallingStrategy implements MarshallingStrategy {
private static final ObjectMapper OBJECT_MAPPER;
static {
OBJECT_MAPPER = new ObjectMapper();
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public <T> T unmarshall(String value, Class<T> classType) throws IOException {
return OBJECT_MAPPER.readValue(value, classType);
}
@Override
public String marshall(Object value) throws JsonProcessingException {
return OBJECT_MAPPER.writeValueAsString(value);
}
@Override
public String getContentType() {
return "application/json";
}
}

View File

@ -0,0 +1,37 @@
package io.linuxserver.fleet.rest.marshalling;
import java.io.IOException;
public interface MarshallingStrategy {
/**
* <p>
* Converts a given string value into its representative object type.
* </p>
* @param value
* The value to convert to an object
* @param classType
* The object class definition
* @return
* The converted object
*/
<T> T unmarshall(String value, Class<T> classType) throws IOException;
/**
* <p>
* Converts an object into a single representative string value.
* </p>
* @param value
* The object to convert
* @return
* The result of the conversion
*/
String marshall(Object value) throws IOException;
/**
* <p>
* The content type of the payloads represented by this strategy.
* </p>
*/
String getContentType();
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.rest.proxy;
import io.linuxserver.fleet.rest.HttpException;
import io.linuxserver.fleet.rest.marshalling.MarshallingStrategy;
import java.io.IOException;
public class LazyLoadPayloadProxy<T> implements PayloadProxy<T> {
private final MarshallingStrategy marshallingStrategy;
private final String payload;
private final Class<T> payloadType;
public LazyLoadPayloadProxy(MarshallingStrategy marshallingStrategy, String payload, Class<T> payloadType) {
this.marshallingStrategy = marshallingStrategy;
this.payload = payload;
this.payloadType = payloadType;
}
@Override
public T get() {
try {
return marshallingStrategy.unmarshall(payload, payloadType);
} catch (IOException e) {
throw new HttpException("Unable to unmarshall response payload", e);
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.rest.proxy;
public interface PayloadProxy<T> {
T get();
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.thread;
public abstract class FleetTask implements Runnable {
private TaskListener taskListener;
public void setTaskListener(TaskListener listener) {
this.taskListener = listener;
}
protected TaskListener getTaskListener() {
return taskListener;
}
@Override
public void run() {
onStart(toString() + " has started.");
executeTask();
onEnd(toString() + " has finished.");
}
protected abstract void executeTask();
@Override
public String toString() {
return getClass().getSimpleName();
}
private void onStart(String message) {
if (taskListener != null)
taskListener.onTaskStart(message);
}
private void onEnd(String message) {
if (taskListener != null)
taskListener.onTaskEnd(message);
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.thread;
import io.linuxserver.fleet.delegate.SynchronisationDelegate;
public class SynchroniseAllRepositoriesTask extends FleetTask {
private final SynchronisationDelegate synchronisationDelegate;
public SynchroniseAllRepositoriesTask(SynchronisationDelegate synchronisationDelegate) {
this.synchronisationDelegate = synchronisationDelegate;
}
@Override
protected void executeTask() {
TaskListener listener = getTaskListener();
if (listener == null)
synchronisationDelegate.synchronise();
else
synchronisationDelegate.synchronise((event) -> listener.onTaskOutput(event.toString()));
}
}

View File

@ -0,0 +1,10 @@
package io.linuxserver.fleet.thread;
public interface TaskListener {
void onTaskStart(String message);
void onTaskOutput(String output);
void onTaskEnd(String message);
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.thread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TaskManager {
private static final Logger LOGGER = LoggerFactory.getLogger(TaskManager.class);
private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
public void scheduleRecurringTask(FleetTask task, int interval, TimeUnit timeUnit) {
LOGGER.info("Scheduling task " + task);
executorService.scheduleAtFixedRate(task, 0, interval, timeUnit);
}
public void runTaskOnce(FleetTask task) {
start(task);
}
private void start(FleetTask task) {
new Thread(task).start();
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import spark.ResponseTransformer;
public class JsonTransformer implements ResponseTransformer {
private static final ObjectMapper OBJECT_MAPPER;
static {
OBJECT_MAPPER = new ObjectMapper();
}
@Override
public String render(Object model) {
try {
return OBJECT_MAPPER.writeValueAsString(model);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,6 @@
package io.linuxserver.fleet.web;
public interface SessionAttribute {
String USER = "io.linuxserver.fleet.web.SessionAttribute.USER";
}

View File

@ -0,0 +1,97 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web;
import io.linuxserver.fleet.auth.AuthenticatedUser;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import spark.Route;
import spark.RouteGroup;
import spark.Session;
import spark.TemplateViewRoute;
import spark.template.freemarker.FreeMarkerEngine;
import static spark.Spark.*;
public class WebServer {
private final int appPort;
public WebServer(int appPort) {
this.appPort = appPort;
}
public void start() {
port(appPort);
staticFiles.location("/assets");
staticFiles.expireTime(600);
path("/admin", configureAuthorisationRoute(""));
path("/admin", configureAuthorisationRoute("/repositories"));
path("/admin", configureAuthorisationRoute("/images"));
path("/admin", configureAuthorisationRoute("/manageImage"));
path("/admin", configureAuthorisationRoute("/manageRepository"));
after("/api/v1/*", (request, response) -> {
response.header("Content-Type", "application/json");
});
exception(FleetApiException.class, (exception, request, response) -> {
response.body(new JsonTransformer().render(new ApiResponse<>("ERROR", exception.getMessage())));
response.header("Content-Type", "application/json");
response.status(exception.getStatusCode());
});
}
public void addPage(String path, TemplateViewRoute page) {
get(path, page, new FreeMarkerEngine());
}
public void addPostRoute(String path, Route route) {
post(path, route);
}
public void addGetApi(String path, Route route) {
get(path, route, new JsonTransformer());
}
public void addPostApi(String path, Route route) {
post(path, route, new JsonTransformer());
}
private RouteGroup configureAuthorisationRoute(String path) {
return () -> before(path, (request, response) -> {
Session session = request.session(false);
if (null == session)
response.redirect("/admin/login");
else {
AuthenticatedUser user = session.attribute(SessionAttribute.USER);
if (null == user)
response.redirect("/admin/login");
}
});
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.pages;
import io.linuxserver.fleet.delegate.ImageDelegate;
import io.linuxserver.fleet.delegate.RepositoryDelegate;
import io.linuxserver.fleet.model.Repository;
import io.linuxserver.fleet.model.RepositoryWithImages;
import spark.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HomePage extends WebPage {
private final RepositoryDelegate repositoryDelegate;
private final ImageDelegate imageDelegate;
public HomePage(RepositoryDelegate repositoryDelegate, ImageDelegate imageDelegate) {
this.repositoryDelegate = repositoryDelegate;
this.imageDelegate = imageDelegate;
}
@Override
protected ModelAndView handle(Request request) {
Map<String, Object> model = new HashMap<>();
List<RepositoryWithImages> populatedRepositories = new ArrayList<>();
List<Repository> repositories = repositoryDelegate.fetchAllRepositories();
for (Repository repository : repositories) {
if (repository.isSyncEnabled())
populatedRepositories.add(new RepositoryWithImages(repository, imageDelegate.fetchImagesByRepository(repository.getId())));
}
model.put("populatedRepositories", populatedRepositories);
return new ModelAndView(model, "home.ftl");
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.pages;
import spark.ModelAndView;
import spark.Request;
import java.util.HashMap;
import java.util.Map;
public class LoginPage extends WebPage {
@Override
protected ModelAndView handle(Request request) {
String failFlag = request.params("fail");
Map<Object, Object> model = new HashMap<>();
if (null != failFlag)
model.put("authFailure", failFlag);
return new ModelAndView(model, "login.ftl");
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.pages;
import io.linuxserver.fleet.delegate.RepositoryDelegate;
import spark.ModelAndView;
import spark.Request;
import java.util.HashMap;
import java.util.Map;
public class ManageRepositoriesPage extends WebPage {
private final RepositoryDelegate repositoryDelegate;
public ManageRepositoriesPage(RepositoryDelegate repositoryDelegate) {
this.repositoryDelegate = repositoryDelegate;
}
@Override
protected ModelAndView handle(Request request) {
Map<String, Object> model = new HashMap<>();
model.put("repositories", repositoryDelegate.fetchAllRepositories());
return new ModelAndView(model, "admin.ftl");
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.pages;
import io.linuxserver.fleet.web.SessionAttribute;
import spark.*;
import java.util.Map;
public abstract class WebPage implements TemplateViewRoute {
@Override
@SuppressWarnings("unchecked")
public ModelAndView handle(Request request, Response response) {
ModelAndView modelAndView = handle(request);
Session session = request.session(false);
if (null != session && null != session.attribute(SessionAttribute.USER))
((Map<String, Object>) modelAndView.getModel()).put("__AUTHENTICATED_USER", session.attribute(SessionAttribute.USER));
return modelAndView;
}
protected abstract ModelAndView handle(Request request);
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.routes;
import io.linuxserver.fleet.delegate.ImageDelegate;
import io.linuxserver.fleet.delegate.RepositoryDelegate;
import io.linuxserver.fleet.model.*;
import io.linuxserver.fleet.model.api.ApiImage;
import io.linuxserver.fleet.model.api.ApiImagesWithTotalCount;
import io.linuxserver.fleet.model.api.ApiResponse;
import spark.Request;
import spark.Response;
import spark.Route;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class AllImagesApi implements Route {
private final RepositoryDelegate repositoryDelegate;
private final ImageDelegate imageDelegate;
public AllImagesApi(RepositoryDelegate repositoryDelegate, ImageDelegate imageDelegate) {
this.repositoryDelegate = repositoryDelegate;
this.imageDelegate = imageDelegate;
}
@Override
public Object handle(Request request, Response response) {
Map<String, List<ApiImage>> mappedImages = new HashMap<>();
long totalCount = 0;
List<Repository> repositories = repositoryDelegate.fetchAllRepositories();
for (Repository repository : repositories) {
if (repository.isSyncEnabled()) {
List<Image> savedImages = imageDelegate.fetchImagesByRepository(repository.getId());
List<ApiImage> apiImages = new ArrayList<>();
for (Image savedImage : savedImages) {
if (!savedImage.isHidden()) {
totalCount += savedImage.getPullCount();
apiImages.add(ApiImage.fromImage(savedImage));
}
}
mappedImages.put(repository.getName(), apiImages);
}
}
return new ApiResponse<>("OK", new ApiImagesWithTotalCount(totalCount, mappedImages));
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.routes;
import io.linuxserver.fleet.auth.AuthenticationResult;
import io.linuxserver.fleet.delegate.AuthenticationDelegate;
import io.linuxserver.fleet.web.SessionAttribute;
import spark.Request;
import spark.Response;
import spark.Route;
import spark.Session;
public class LoginRoute implements Route {
private final AuthenticationDelegate authenticationDelegate;
public LoginRoute(AuthenticationDelegate authenticationDelegate) {
this.authenticationDelegate = authenticationDelegate;
}
@Override
public Object handle(Request request, Response response) {
Session session = request.session(false);
if (isUserAlreadyLoggedIn(session)) {
response.redirect("/");
return null;
}
String username = request.queryParams("username");
String password = request.queryParams("password");
AuthenticationResult authResult = authenticationDelegate.authenticate(username, password);
if (!authResult.isAuthenticated()) {
response.redirect("/admin/login?fail=true");
return null;
}
final Session newSession = request.session();
newSession.attribute(SessionAttribute.USER, authResult.getUser());
response.redirect("/");
return "OK";
}
private boolean isUserAlreadyLoggedIn(Session session) {
return null != session && null != session.attribute(SessionAttribute.USER);
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.routes;
import spark.Request;
import spark.Response;
import spark.Route;
import spark.Session;
public class LogoutRoute implements Route {
@Override
public Object handle(Request request, Response response) {
Session session = request.session(false);
if (null != session)
session.invalidate();
response.redirect("/");
return null;
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.routes;
import io.linuxserver.fleet.delegate.ImageDelegate;
import io.linuxserver.fleet.exception.SaveException;
import io.linuxserver.fleet.model.Image;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import spark.Request;
import spark.Response;
import spark.Route;
public class ManageImageApi implements Route {
private final ImageDelegate imageDelegate;
public ManageImageApi(ImageDelegate imageDelegate) {
this.imageDelegate = imageDelegate;
}
@Override
public Object handle(Request request, Response response) {
try {
int imageId = Integer.parseInt(request.queryParams("imageId"));
Image image = imageDelegate.fetchImage(imageId);
if (null == image) {
response.status(404);
response.header("Content-Type", "application/json");
return new ApiResponse<>("Error", "Image not found.");
}
Action action = Action.valueOf(request.queryParams("action"));
switch (action) {
case SHOW:
case HIDE:
image.withHidden(Action.HIDE.equals(action));
break;
case STABLE:
case UNSTABLE:
image.withUnstable(Action.UNSTABLE.equals(action));
break;
}
imageDelegate.saveImage(image);
response.header("Content-Type", "application/json");
return new ApiResponse<>("OK", "Image updated.");
} catch (Exception e) {
throw new FleetApiException(500, e.getMessage(), e);
}
}
public enum Action {
SHOW, HIDE, MASK, STABLE, UNSTABLE
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.routes;
import io.linuxserver.fleet.delegate.RepositoryDelegate;
import io.linuxserver.fleet.model.Repository;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import spark.Request;
import spark.Response;
import spark.Route;
public class ManageRepositoryApi implements Route {
private final RepositoryDelegate repositoryDelegate;
public ManageRepositoryApi(RepositoryDelegate repositoryDelegate) {
this.repositoryDelegate = repositoryDelegate;
}
@Override
public Object handle(Request request, Response response) {
try {
int repositoryId = Integer.parseInt(request.queryParams("repositoryId"));
Repository repository = repositoryDelegate.fetchRepository(repositoryId);
if (null == repository) {
response.status(404);
response.header("Content-Type", "application/json");
return new ApiResponse<>("Error", "Repository not found.");
}
Action action = Action.valueOf(request.queryParams("action"));
switch (action) {
case ENABLE_SYNC:
case DISABLE_SYNC:
repository.withSyncEnabled(Action.ENABLE_SYNC.equals(action));
break;
case MASK:
String versionMask = cleanParam(request.queryParams("versionMask"));
repository.withVersionMask(versionMask);
break;
}
repositoryDelegate.saveRepository(repository);
response.header("Content-Type", "application/json");
return new ApiResponse<>("OK", "Repository updated.");
} catch (Exception e) {
throw new FleetApiException(500, e.getMessage(), e);
}
}
private String cleanParam(String param) {
if ("".equalsIgnoreCase(param))
return null;
return param;
}
public enum Action {
ENABLE_SYNC, DISABLE_SYNC, MASK
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,204 @@
body {
color: #666;
font-family: 'Nunito', sans-serif !important;
font-size: 14px;
/*background-color: #f2f3f8;*/
}
a {
color: #666;
}
h1, h2, h3, h4, h5 {
color: #333;
font-family: 'Nunito', sans-serif;
font-weight: 600;
}
h1 {
font-size: 26px;
}
h2 {
font-size: 26px;
}
h3 {
font-size: 16px;
}
.navbar-brand {
font-family: 'Pacifico', cursive;
font-size: 30px;
}
.navbar-brand .small {
font-family: 'Nunito', sans-serif;
font-size: 14px;
color: #666;
}
.navbar-darkish {
background-color: #cdcdcd;
-webkit-box-shadow: 0px 0px 14px 0px rgba(82,63,105,0.05);
-moz-box-shadow: 0px 0px 14px 0px rgba(82,63,105,0.05);
box-shadow: 0px 0px 14px 0px rgba(82,63,105,0.05);
}
.navbar-white {
box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);
border-bottom: 1px solid #d4dadf;
background-color: #FFFFFF;
}
.table thead tr th {
border-top: 0;
padding-bottom: 0;
text-transform: uppercase;
font-size: 12px;
}
.table tbody tr td {
border-color: #eaeaea;
}
.table tbody tr.hidden-image, .table tbody tr.hidden-image a, .table tbody tr.hidden-image code {
color: rgba(0, 0, 0, 0.3)
}
.table tbody tr td.image-name {
font-weight: 600;
font-size: 16px;
}
code {
background-color: #efefef;
border-radius: 2px;
color: #333;
font-size: 12px;
padding: 5px;
}
.container--image-list {
margin-top: 50px;
}
/*
.container--white {
background-color: #FFF;
-webkit-box-shadow: 0px 0px 14px 0px rgba(82,63,105,0.05);
-moz-box-shadow: 0px 0px 14px 0px rgba(82,63,105,0.05);
box-shadow: 0px 0px 14px 0px rgba(82,63,105,0.05);
border-radius: 5px;
}
*/
.number, .version-mask {
font-family: monospace;
}
.col--shaded {
background-color: #fff;
border-radius: 1px;
border: 1px solid #efefef;
}
.btn-xsm {
padding: .20rem .45rem;
font-size: .700rem;
line-height: 1.4;
border-radius: .2rem;
}
.btn-clickable {
cursor: pointer;
}
.tablesorter-header {
cursor: pointer;
}
.tablesorter-header:focus {
outline: none;
}
/*
*****
iOS Switches copied from: https://codepen.io/aorcsik/pen/OPMyQp
*****
*/
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 26px;
margin: 0;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(18px);
-ms-transform: translateX(18px);
transform: translateX(18px);
}
.slider.round {
border-radius: 26px;
}
.slider.round:before {
border-radius: 50%;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,193 @@
/**
* Bootstrap theme v3.x
*
* WARNING!... once a stable Bootstrap v4.x is released,
* this file will be removed; use theme.bootstrap_3.css
*/
.tablesorter-bootstrap {
width: 100%;
}
.tablesorter-bootstrap thead th,
.tablesorter-bootstrap thead td,
.tablesorter-bootstrap tfoot th,
.tablesorter-bootstrap tfoot td {
font: 14px/20px Arial, Sans-serif;
font-weight: bold;
padding: 4px;
margin: 0 0 18px;
background-color: #eee;
}
.tablesorter-bootstrap .tablesorter-header {
cursor: pointer;
}
.tablesorter-bootstrap .sorter-false {
cursor: default;
}
.tablesorter-bootstrap .tablesorter-header.sorter-false i.tablesorter-icon {
display: none;
}
.tablesorter-bootstrap .tablesorter-header-inner {
position: relative;
padding: 4px 18px 4px 4px;
}
.tablesorter-bootstrap .sorter-false .tablesorter-header-inner {
padding: 4px;
}
/* bootstrap uses <i> for icons */
.tablesorter-bootstrap .tablesorter-header i.tablesorter-icon {
font-size: 11px;
position: absolute;
right: 2px;
top: 50%;
margin-top: -7px; /* half the icon height; older IE doesn't like this */
width: 14px;
height: 14px;
background-repeat: no-repeat;
line-height: 14px;
display: inline-block;
}
/* black unsorted icon */
.tablesorter-bootstrap .bootstrap-icon-unsorted {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAAA20lEQVR4AWJABpKSkoxALCstLb0aUAsZaCAMhVEY6B0amx8YZWDDEDSBa2AGe7XeIiAAClYwVGBvsAcIllsf/mvcC9DgOOd8h90fxWvngVEUbZIkuWRZZlE8eQjcisgZMM9zi+LJ6ZfwegmWZflZDugdHMfxTcGqql7TNBlUB/QObtv2VBSFrev6OY7jngzFk9OT/fn73fWYpqnlXNyXDMWT0zuYx/Bvel9ej+LJ6R08DMOu67q7DkTkrSA5vYPneV71fX/QASdTkJwezhs0TfMARn0wMDDGXEPgF4oijqwM5YjNAAAAAElFTkSuQmCC);
}
/* white unsorted icon; updated to use bootstrap-icon-white - see #1432 */
.tablesorter-bootstrap .bootstrap-icon-white.bootstrap-icon-unsorted {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAAAe0lEQVR4AbXQoRWDMBiF0Sh2QLAAQ8SxJGugWSA6A2STW1PxTsnB9cnkfuYvv8OGC1t5G3Y0QMP+Bm857keAdQIzWBP3+Bw4MADQE18B6/etRnCV/w9nnGuLezfAmXhABGtAGIkruvk6auIFRwQJDywllsEAjCecB20GP59BQQ+gtlRLAAAAAElFTkSuQmCC);
}
/* since bootstrap (table-striped) uses nth-child(), we just use this to add a zebra stripe color */
.tablesorter-bootstrap > tbody > tr.odd > td,
.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.odd:hover ~ tr.tablesorter-hasChildRow.odd ~ .tablesorter-childRow.odd > td {
background-color: #f9f9f9;
}
.tablesorter-bootstrap > tbody > tr.hover > td,
.tablesorter-bootstrap > tbody > tr.odd:hover > td,
.tablesorter-bootstrap > tbody > tr.even:hover > td,
.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.odd:hover ~ .tablesorter-childRow.odd > td,
.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.even:hover ~ .tablesorter-childRow.even > td {
background-color: #f5f5f5;
}
.tablesorter-bootstrap > tbody > tr.even > td,
.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.even:hover ~ tr.tablesorter-hasChildRow.even ~ .tablesorter-childRow.even > td {
background-color: #fff;
}
/* processing icon */
.tablesorter-bootstrap .tablesorter-processing {
background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=');
background-position: center center !important;
background-repeat: no-repeat !important;
}
/* Column Widget - column sort colors */
.tablesorter-bootstrap > tbody > tr.odd td.primary {
background-color: #bfbfbf;
}
.tablesorter-bootstrap > tbody > tr td.primary,
.tablesorter-bootstrap > tbody > tr.even td.primary {
background-color: #d9d9d9;
}
.tablesorter-bootstrap > tbody > tr.odd td.secondary {
background-color: #d9d9d9;
}
.tablesorter-bootstrap > tbody > tr td.secondary,
.tablesorter-bootstrap > tbody > tr.even td.secondary {
background-color: #e6e6e6;
}
.tablesorter-bootstrap > tbody > tr.odd td.tertiary {
background-color: #e6e6e6;
}
.tablesorter-bootstrap > tbody > tr td.tertiary,
.tablesorter-bootstrap > tbody > tr.even td.tertiary {
background-color: #f2f2f2;
}
/* caption */
.tablesorter-bootstrap > .caption {
background-color: #fff;
}
/* filter widget */
.tablesorter-bootstrap .tablesorter-filter-row input.tablesorter-filter,
.tablesorter-bootstrap .tablesorter-filter-row select.tablesorter-filter {
width: 98%;
margin: 0;
padding: 4px 6px;
color: #333;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: height 0.1s ease;
-moz-transition: height 0.1s ease;
-o-transition: height 0.1s ease;
transition: height 0.1s ease;
}
.tablesorter-bootstrap .tablesorter-filter-row .tablesorter-filter.disabled {
background-color: #eee;
color: #555;
cursor: not-allowed;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset;
box-sizing: border-box;
transition: height 0.1s ease;
}
.tablesorter-bootstrap .tablesorter-filter-row {
background-color: #efefef;
}
.tablesorter-bootstrap .tablesorter-filter-row td {
background-color: #efefef;
line-height: normal;
text-align: center;
padding: 4px 6px;
vertical-align: middle;
-webkit-transition: line-height 0.1s ease;
-moz-transition: line-height 0.1s ease;
-o-transition: line-height 0.1s ease;
transition: line-height 0.1s ease;
}
/* hidden filter row */
.tablesorter-bootstrap .tablesorter-filter-row.hideme td {
padding: 2px; /* change this to modify the thickness of the closed border row */
margin: 0;
line-height: 0;
}
.tablesorter-bootstrap .tablesorter-filter-row.hideme * {
height: 1px;
min-height: 0;
border: 0;
padding: 0;
margin: 0;
/* don't use visibility: hidden because it disables tabbing */
opacity: 0;
filter: alpha(opacity=0);
}
/* rows hidden by filtering */
.tablesorter .filtered {
display: none;
}
/* pager plugin */
.tablesorter-bootstrap .tablesorter-pager select {
padding: 4px 6px;
}
.tablesorter-bootstrap .tablesorter-pager .pagedisplay {
border: 0;
}
/* tfoot i for pager controls */
.tablesorter-bootstrap tfoot i {
font-size: 11px;
}
/* ajax error row */
.tablesorter .tablesorter-errorRow td {
text-align: center;
cursor: pointer;
background-color: #e6bf99;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,207 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var ajaxManager = (function($) {
var handleError = function(jqXHR, textStatus, handleError) {
var error = JSON.parse(jqXHR.responseText);
console.log(error);
};
var call = function(param, onDone) {
return $.ajax(param).done(onDone).fail(handleError);
};
return {
call: call
};
}(jQuery));
var repositoryManager = (function($) {
var updateSyncSettings = function() {
var syncSwitch = $(this);
var row = syncSwitch.parents('tr');
var repositoryId = row.data('repository-id');
var syncEnabled = syncSwitch.is(':checked');
var request = {
url: '/admin/manageRepository',
method: 'POST',
data: {
repositoryId: repositoryId,
action: syncEnabled ? 'ENABLE_SYNC' : 'DISABLE_SYNC'
}
};
ajaxManager.call(request, function(data) { console.log("Updated") });
};
var updateVersionMask = function() {
var versionMask = $(this);
var row = versionMask.parents('tr');
var repositoryId = row.data('repository-id');
var maskValue = versionMask.val();
var request = {
url: '/admin/manageRepository',
method: 'POST',
data: {
repositoryId: repositoryId,
action: 'MASK',
versionMask: maskValue
}
};
ajaxManager.call(request, function(data) { console.log("Updated") });
};
var init = function() {
$('.sync-repository').on('change', updateSyncSettings);
$('.version-mask').on('blur', updateVersionMask);
};
return {
init: init
};
}(jQuery));
var imageListManager = (function($) {
var showImage = function() {
var option = $(this);
var row = getImageRow(option);
var imageId = getImageId(row);
var request = buildRequest('SHOW', imageId)
ajaxManager.call(request, function(data) {
row.removeClass('hidden-image');
option.replaceWith(createHideButton());
});
};
var hideImage = function() {
var option = $(this);
var row = getImageRow(option);
var imageId = getImageId(row);
var request = buildRequest('HIDE', imageId)
var call = ajaxManager.call(request, function(data) {
row.addClass('hidden-image');
option.replaceWith(createShowButton());
});
};
var markImageUnstable = function() {
var option = $(this);
var row = getImageRow(option);
var imageId = getImageId(row);
var request = buildRequest('UNSTABLE', imageId)
var call = ajaxManager.call(request, function(data) {
row.find('.image-status').find('i.fas').replaceWith(createUnstableIcon());
option.replaceWith(createStableButton());
});
};
var markImageStable = function() {
var option = $(this);
var row = getImageRow(option);
var imageId = getImageId(row);
var request = buildRequest('STABLE', imageId)
var call = ajaxManager.call(request, function(data) {
row.find('.image-status').find('i.fas').replaceWith(createStableIcon());
option.replaceWith(createUnstableButton());
});
};
var buildRequest = function(action, imageId) {
return {
url: '/admin/manageImage',
method: 'POST',
data: {
action: action,
imageId: imageId
}
};
};
var createHideButton = function() {
return $('<button type="button" class="image--hide dropdown-item btn-clickable">Hide from list</button>');
};
var createShowButton = function() {
return $('<button type="button" class="image--show dropdown-item btn-clickable">Show in list</button>');
};
var createStableButton = function() {
return $('<button type="button" class="image--mark-stable dropdown-item btn-clickable">Mark as stable</button>');
};
var createUnstableButton = function() {
return $('<button type="button" class="image--mark-unstable dropdown-item btn-clickable">Mark as unstable</button>');
};
var createUnstableIcon = function() {
return $('<i class="fas fa-exclamation-triangle text-warning" title="Potentially unstable"></i>');
};
var createStableIcon = function() {
return $('<i class="fas fa-check-circle text-success" title="No issues reported"></i>');
};
var getImageRow = function(item) {
return item.parents('tr');
};
var getImageId = function(row) {
return parseInt(row.data('image-id'));
};
var init = function() {
$('.admin-actions').on('click', '.image--show', showImage);
$('.admin-actions').on('click', '.image--hide', hideImage);
$('.admin-actions').on('click', '.image--mark-stable', markImageStable);
$('.admin-actions').on('click', '.image--mark-unstable', markImageUnstable);
};
return {
init: init
}
}(jQuery));
imageListManager.init();
repositoryManager.init();

Some files were not shown because too many files have changed in this diff Show More