Core Meta

- Added logo uploads.
This commit is contained in:
Josh Stark 2020-01-19 11:19:16 +00:00
parent b8f64bd4f2
commit bd1946f84e
21 changed files with 292 additions and 22 deletions

3
.gitignore vendored
View File

@ -28,6 +28,7 @@ build/
out/
*.iml
config/fleet.properties
/config/fleet_static/*
src/main/resources/assets/js/all*.js
src/main/resources/assets/css/all*.css
src/main/resources/log4j2.xml
@ -35,4 +36,4 @@ src/main/resources/log4j2.xml
.classpath
.project
.settings/
.settings/

View File

@ -29,6 +29,7 @@ import io.linuxserver.fleet.v2.client.docker.queue.DockerApiDelegate;
import io.linuxserver.fleet.v2.db.DefaultImageDAO;
import io.linuxserver.fleet.v2.db.DefaultScheduleDAO;
import io.linuxserver.fleet.v2.db.DefaultUserDAO;
import io.linuxserver.fleet.v2.file.FileManager;
import io.linuxserver.fleet.v2.key.ImageKey;
import io.linuxserver.fleet.v2.service.ImageService;
import io.linuxserver.fleet.v2.service.ScheduleService;
@ -53,10 +54,12 @@ public class FleetAppController extends AbstractAppController implements Service
private final SynchronisationService syncService;
private final UserService userService;
private final AuthenticationDelegate authenticationDelegate;
private final FileManager fileManager;
public FleetAppController() {
imageService = new ImageService(new DefaultImageDAO(getDatabaseProvider()));
fileManager = new FileManager(this);
imageService = new ImageService(this, new DefaultImageDAO(getDatabaseProvider()));
scheduleService = new ScheduleService(this, new DefaultScheduleDAO(getDatabaseProvider()));
dockerApiDelegate = new DockerApiDelegate(this);
syncService = new SynchronisationService(this);
@ -156,6 +159,11 @@ public class FleetAppController extends AbstractAppController implements Service
return userService;
}
@Override
public FileManager getFileManager() {
return fileManager;
}
public final AuthenticationResult authenticateUser(final String username, final String password) {
return authenticationDelegate.authenticate(username, password);
}

View File

@ -19,6 +19,11 @@ package io.linuxserver.fleet.core;
public interface FleetRuntime {
/**
* If set will switch specific properties to allow more streamlined development
*/
boolean DEV_MODE = System.getProperty("enable.dev") != null;
/**
* Base directory for the config file.
*/

View File

@ -53,6 +53,7 @@ class PropertiesLoader extends BaseRuntimeLoader {
Properties properties = new Properties();
properties.load(new FileInputStream(FleetRuntime.CONFIG_BASE + "/fleet.properties"));
properties.load(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream("version.properties")));
properties.setProperty("fleet.static.dirname", "fleet_static");
this.properties = new AppProperties(properties);
@ -88,7 +89,7 @@ class PropertiesLoader extends BaseRuntimeLoader {
private boolean createStaticFileDirectory() {
File staticFilesDir = new File(FleetRuntime.CONFIG_BASE + "/fleet_static");
File staticFilesDir = new File(properties.getStaticFilesPath().toString());
if (staticFilesDir.exists()) {
return true;

View File

@ -17,6 +17,7 @@
package io.linuxserver.fleet.core;
import io.linuxserver.fleet.v2.file.FileManager;
import io.linuxserver.fleet.v2.service.ImageService;
import io.linuxserver.fleet.v2.service.ScheduleService;
import io.linuxserver.fleet.v2.service.SynchronisationService;
@ -31,4 +32,6 @@ public interface ServiceProvider {
ScheduleService getScheduleService();
UserService getUserService();
FileManager getFileManager();
}

View File

@ -17,6 +17,10 @@
package io.linuxserver.fleet.core.config;
import io.linuxserver.fleet.core.FleetRuntime;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
public class AppProperties {
@ -59,6 +63,10 @@ public class AppProperties {
return getStringProperty("fleet.database.password");
}
public final Path getStaticFilesPath() {
return Paths.get(FleetRuntime.CONFIG_BASE, getStringProperty("fleet.static.dirname")).toAbsolutePath();
}
public String getAppSecret() {
String secret = getStringProperty("fleet.admin.secret");

View File

@ -0,0 +1,121 @@
/*
* Copyright (c) 2020 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.v2.file;
import io.linuxserver.fleet.core.FleetAppController;
import io.linuxserver.fleet.v2.key.ImageKey;
import io.linuxserver.fleet.v2.service.AbstractAppService;
import io.linuxserver.fleet.v2.types.FilePathDetails;
import io.linuxserver.fleet.v2.types.internal.ImageAppLogo;
import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileManager extends AbstractAppService {
private static final String imageDirName = "images";
private final String publicSafeImagesDir;
private final Path staticImagesDir;
public FileManager(final FleetAppController controller) {
super(controller);
staticImagesDir = Paths.get(getProperties().getStaticFilesPath().toString(), imageDirName);
publicSafeImagesDir = "/" + imageDirName;
makeImageUploadDir();
}
public final FilePathDetails saveImageLogo(final ImageAppLogo logo) {
if (logo.getMimeType().startsWith("image/")) {
try {
final FilePathDetails filePathDetails = makeFilePathDetails(logo);
final File logoFile = new File(filePathDetails.getFullAbsolutePathWithFileName());
if (logoFile.exists()) {
final boolean deleted = logoFile.delete();
if (!deleted) {
getLogger().warn("Unable to delete file: " + logoFile);
return null;
}
}
boolean created = logoFile.createNewFile();
if (created) {
writeDataToFile(logo, logoFile);
return filePathDetails;
} else {
getLogger().warn("Unable to delete file: " + logoFile);
return null;
}
} catch (IOException e) {
getLogger().error("Unable to create logo file.", e);
throw new RuntimeException(e);
}
} else {
throw new IllegalArgumentException("Disallowed mimeType for file: " + logo.getMimeType());
}
}
private FilePathDetails makeFilePathDetails(final ImageAppLogo logo) {
return new FilePathDetails(makePathSafeFileName(logo.getImageKey()) + logo.getFileExtension(),
staticImagesDir.toString(),
publicSafeImagesDir);
}
private String makePathSafeFileName(final ImageKey key) {
return key.getAsRepositoryAndImageName().replace("/", "_");
}
private void writeDataToFile(final ImageAppLogo logo, final File logoFile) throws IOException {
final byte[] buffer = new byte[logo.getRawDataStream().available()];
int read = logo.getRawDataStream().read(buffer);
if (read != -1) {
getLogger().warn("Not all file content has been read! File may be corrupted once saved to disk");
}
try (final OutputStream out = new FileOutputStream(logoFile)) {
out.write(buffer);
}
}
private void makeImageUploadDir() {
final File imageDir = new File(staticImagesDir.toString());
if (!imageDir.exists()) {
getLogger().info("Creating new image directory for uploaded logos");
final boolean created = imageDir.mkdir();
if (!created) {
throw new RuntimeException("Unable to create uploaded file dir. Check permissions");
}
}
}
}

View File

@ -18,6 +18,7 @@
package io.linuxserver.fleet.v2.service;
import io.linuxserver.fleet.core.FleetAppController;
import io.linuxserver.fleet.core.config.AppProperties;
import io.linuxserver.fleet.v2.LoggerOwner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -36,6 +37,10 @@ public class AbstractAppService implements LoggerOwner {
return controller;
}
public final AppProperties getProperties() {
return getController().getAppProperties();
}
public final Logger getLogger() {
return logger;
}

View File

@ -17,10 +17,12 @@
package io.linuxserver.fleet.v2.service;
import io.linuxserver.fleet.core.FleetAppController;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.dockerhub.util.DockerTagFinder;
import io.linuxserver.fleet.v2.cache.RepositoryCache;
import io.linuxserver.fleet.v2.db.ImageDAO;
import io.linuxserver.fleet.v2.file.FileManager;
import io.linuxserver.fleet.v2.key.ImageKey;
import io.linuxserver.fleet.v2.key.ImageLookupKey;
import io.linuxserver.fleet.v2.key.RepositoryKey;
@ -34,24 +36,25 @@ import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class ImageService {
public class ImageService extends AbstractAppService {
private static final Logger LOGGER = LoggerFactory.getLogger(ImageService.class);
private final ImageDAO imageDAO;
private final FileManager fileManager;
private final RepositoryCache repositoryCache;
private final TemplateMerger templateMerger;
public ImageService(final ImageDAO imageDAO) {
public ImageService(final FleetAppController controller, final ImageDAO imageDAO) {
super(controller);
this.imageDAO = imageDAO;
this.fileManager = controller.getFileManager();
this.repositoryCache = new RepositoryCache();
this.templateMerger = new TemplateMerger();
@ -246,7 +249,11 @@ public class ImageService {
String appLogoPath = image.getMetaData().getAppImagePath();
if (null != generalInfoUpdateRequest.getImageAppLogo()) {
// TODO: Write FileManager
final FilePathDetails filePathDetails = fileManager.saveImageLogo(generalInfoUpdateRequest.getImageAppLogo());
if (null != filePathDetails) {
appLogoPath = filePathDetails.getPublicSafePathWithFileName();
}
}
final ImageCoreMeta coreMeta = new ImageCoreMeta(appLogoPath,

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2020 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.v2.types;
public class FilePathDetails {
private final String fileNameWithExtension;
private final String fullAbsolutePath;
private final String publicSafePath;
public FilePathDetails(final String fileNameWithExtension,
final String fullAbsolutePath,
final String publicSafePath) {
this.fileNameWithExtension = fileNameWithExtension;
this.fullAbsolutePath = fullAbsolutePath;
this.publicSafePath = publicSafePath;
}
public final String getFileNameWithExtension() {
return fileNameWithExtension;
}
public final String getFullAbsolutePath() {
return fullAbsolutePath;
}
public final String getPublicSafePathWithFileName() {
return publicSafePath + "/" + getFileNameWithExtension();
}
public final String getFullAbsolutePathWithFileName() {
return getFullAbsolutePath() + "/" + getFileNameWithExtension();
}
}

View File

@ -25,18 +25,21 @@ public class ImageAppLogo {
private final ImageKey imageKey;
private final InputStream rawDataStream;
private final String mimeType;
private final String logoName;
private final long logoSize;
private final String fileExtension;
public ImageAppLogo(final ImageKey imageKey,
final InputStream rawDataStream,
final String mimeType,
final String logoName,
final long logoSize,
final String fileExtension) {
this.imageKey = imageKey;
this.rawDataStream = rawDataStream;
this.mimeType = mimeType;
this.logoName = logoName;
this.logoSize = logoSize;
this.fileExtension = fileExtension;
@ -50,6 +53,10 @@ public class ImageAppLogo {
return rawDataStream;
}
public final String getMimeType() {
return mimeType;
}
public final String getLogoName() {
return logoName;
}

View File

@ -19,12 +19,13 @@ package io.linuxserver.fleet.v2.web;
import io.javalin.Javalin;
import io.javalin.core.validation.JavalinValidation;
import io.javalin.http.staticfiles.Location;
import io.linuxserver.fleet.core.FleetAppController;
import io.linuxserver.fleet.core.config.WebConfiguration;
import io.linuxserver.fleet.v2.key.ImageKey;
import io.linuxserver.fleet.v2.key.ImageLookupKey;
import io.linuxserver.fleet.v2.key.RepositoryKey;
import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic;
import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic.StatGroupMode;
import io.linuxserver.fleet.v2.web.routes.*;
import static io.javalin.apibuilder.ApiBuilder.*;
@ -45,18 +46,19 @@ public class WebRouteController {
config.showJavalinBanner = false;
config.addStaticFiles(Locations.Static.Static);
config.addStaticFiles(app.getAppProperties().getStaticFilesPath().toString(), Location.EXTERNAL);
config.accessManager(new DefaultAccessManager());
}).start(webConfiguration.getPort());
Javalin.log.info(printBanner());
JavalinValidation.register(ImagePullStatistic.StatGroupMode.class, ImagePullStatistic.StatGroupMode::valueOf);
JavalinValidation.register(ImageKey.class, ImageKey::parse);
JavalinValidation.register(StatGroupMode.class, StatGroupMode::valueOf);
JavalinValidation.register(ImageKey.class, ImageKey::parse);
JavalinValidation.register(ImageLookupKey.class, ImageLookupKey::new);
JavalinValidation.register(RepositoryKey.class, RepositoryKey::parse);
JavalinValidation.register(RepositoryKey.class, RepositoryKey::parse);
webInstance.exception(ApiException.class, (e, ctx) -> {
webInstance.exception(Exception.class, (e, ctx) -> {
ctx.status(400);
ctx.result(e.getMessage());

View File

@ -71,7 +71,7 @@ public abstract class AbstractPageHandler extends AbstractAppService implements
injectTopLevelModelAttributes(ctx, spec);
checkViewForRedirect(ctx, spec);
} catch (Exception e) {
} catch (Throwable e) {
LOGGER.error("Unexpected error occurred when loading page.", e);
ctx.render("views/pages/error.ftl", model("error", "Something unexepected happened", "exception", e));

View File

@ -101,6 +101,7 @@ public class AdminImageEditController extends AbstractPageHandler {
return new ImageAppLogo(imageKey,
uploadedFile.getContent(),
uploadedFile.getContentType(),
uploadedFile.getFilename(),
uploadedFile.getSize(),
uploadedFile.getExtension());

View File

@ -55,6 +55,10 @@ span.image-title {
margin-left: 1rem;
}
.has-margin-right {
margin-right: 1rem;
}
.has-margin-top {
margin-top: 1rem;
}

View File

@ -1,5 +1,5 @@
#Sat Jan 18 17:28:00 GMT 2020
app.build.date=2020-01-18T17\:28\:00
#Sun Jan 19 10:53:51 GMT 2020
app.build.date=2020-01-19T10\:53\:51
app.build.os=Linux
app.build.user=josh
app.version=2.0.0

View File

@ -72,6 +72,15 @@
<label class="label" for="ImageAppLogo">App Logo</label>
</div>
<div class="field-body">
<#if image.metaData.appImagePath?has_content>
<div class="is-fullwidth">
<figure class="image is-128x128">
<img src="${image.metaData.appImagePath}" alt="${image.name} logo" />
</figure>
</div>
</#if>
<input type="file" name="ImageAppLogo" id="ImageAppLogo" />
</div>
</div>

View File

@ -66,6 +66,7 @@
<@table.table id="ImageTable" isFullWidth=true isScrollable=true extraClasses="table--sortable">
<thead>
<tr>
<th></th>
<th>Name</th>
<th></th>
<th>Latest Version</th>

View File

@ -19,11 +19,13 @@
<#import "../prebuilt/fleet-title.ftl" as title />
<#import "../prebuilt/docker-example.ftl" as dockerExample />
<#import "../ui/layout/section.ftl" as section />
<#import "../ui/layout/container.ftl" as container />
<#import "../ui/components/message.ftl" as message />
<#import "../ui/elements/box.ftl" as box />
<#import "../ui/elements/button.ftl" as button />
<#import "../ui/elements/table.ftl" as table />
<#import "../ui/elements/tag.ftl" as tag />
<#import "../ui/layout/container.ftl" as container />
<#import "../ui/layout/section.ftl" as section />
<@base.base title="${(image.fullName)!'Unknown Image'}" context="image" hasHero=false>
@ -37,7 +39,12 @@
<div class="column is-full">
<@title.title icon="cube" thinValue=image.repositoryName boldValue=image.name separator="/" subtitle=image.description />
<@title.title
icon="cube"
imageIcon=image.metaData.appImagePath
thinValue=image.repositoryName
boldValue=image.name separator="/"
subtitle=image.description />
<div class="tags">
@ -191,6 +198,20 @@
<h3 class="subtitle is-6">
Basic examples for getting this image running as a container
</h3>
<@message.message colour="info">
These examples <strong>do not</strong> include the relevant values for volume mappings or environment variables. You will
need to review these snippets and fill in the gaps based on your own needs. If you would like to generate a compose
block or CLI run command with your mappings included, you can also use the template generator:
<div class="has-text-centered has-margin-top">
<@button.link id="TemplateGeneratorLink" colour="info">
<i class="fas fa-layer-group"></i> Template Generator
</@button.link>
</div>
</@message.message>
</div>
<div class="column is-full has-margin-bottom">

View File

@ -14,10 +14,17 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<#macro title thinValue="" boldValue="" separator="" icon="" subtitle="">
<#macro title thinValue="" boldValue="" separator="" icon="" imageIcon="" subtitle="">
<h2 class="title is-size-3-desktop is-size-4-mobile">
<#if icon?has_content><i class="fas fa-${icon}"></i> </#if><#if thinValue?has_content><span class="has-text-weight-light">${thinValue}</span>${separator}</#if>${boldValue}<span class="has-text-primary">.</span>
<#if imageIcon?has_content>
<figure class="image is-32x32 is-pulled-left has-margin-right">
<img src="${imageIcon}" alt="Title logo" />
</figure>
<#elseif icon?has_content>
<i class="fas fa-${icon}"></i>
</#if>
<#if thinValue?has_content><span class="has-text-weight-light">${thinValue}</span>${separator}</#if>${boldValue}<span class="has-text-primary">.</span>
<#nested />
</h2>
<#if subtitle?has_content>

View File

@ -22,10 +22,19 @@
<#if !image.hidden>
<tr class="image-row" data-image-name="${image.name}">
<td class="is-vcentered has-text-right is-paddingless" style="width: 16px">
<#if image.metaData.appImagePath?has_content>
<figure class="image is-16x16">
<img src="${image.metaData.appImagePath}" alt="Title logo" />
</figure>
<#else>
<i class="fas fa-cube"></i>
</#if>
</td>
<td class="is-vcentered">
<h4 class="title is-6">
<a class="has-text-grey-dark" href="/image?name=${image.fullName}">
<i class="fas fa-cube"></i> <span class="has-text-weight-light">${image.repositoryKey.name} / </span><span class="has-text-weight-bold">${image.name}</span>
<span class="has-text-weight-light">${image.repositoryKey.name} / </span><span class="has-text-weight-bold">${image.name}</span>
</a>
</h4>
</td>