From b8f64bd4f2539c52c8dfef258528292344a9cf1b Mon Sep 17 00:00:00 2001 From: Josh Stark Date: Sat, 18 Jan 2020 17:31:21 +0000 Subject: [PATCH] Core Meta - Store core meta as part of standard image save - Display core meta on main image display - TODO: upload image and save. --- .../queue/DockerImageUpdateResponse.java | 2 +- .../fleet/v2/db/DefaultImageDAO.java | 26 +++- .../fleet/v2/service/ImageService.java | 29 +++- .../v2/service/SynchronisationService.java | 2 + .../fleet/v2/service/util/TemplateMerger.java | 8 +- .../types/internal/AbstractParamRequest.java | 92 +++++++++++ .../fleet/v2/types/internal/ImageAppLogo.java | 64 ++++++++ .../ImageGeneralInfoUpdateRequest.java | 52 +++++++ .../types/internal/ImageTemplateRequest.java | 86 ++--------- .../fleet/v2/types/meta/ImageCoreMeta.java | 62 ++++++++ .../fleet/v2/types/meta/ImageMetaData.java | 45 +++++- .../web/routes/AdminImageEditController.java | 25 +++ .../V2.3__UpdateImageViewForCoreMeta.sql | 145 ++++++++++++++++++ src/main/resources/version.properties | 4 +- .../views/pages/admin/image-edit.ftl | 35 +++-- src/main/resources/views/pages/image.ftl | 35 ++++- .../views/prebuilt/docker-example.ftl | 12 +- src/main/resources/views/ui/form/input.ftl | 9 +- 18 files changed, 621 insertions(+), 112 deletions(-) create mode 100644 src/main/java/io/linuxserver/fleet/v2/types/internal/AbstractParamRequest.java create mode 100644 src/main/java/io/linuxserver/fleet/v2/types/internal/ImageAppLogo.java create mode 100644 src/main/java/io/linuxserver/fleet/v2/types/internal/ImageGeneralInfoUpdateRequest.java create mode 100644 src/main/java/io/linuxserver/fleet/v2/types/meta/ImageCoreMeta.java create mode 100644 src/main/resources/db/migration/V2.3__UpdateImageViewForCoreMeta.sql diff --git a/src/main/java/io/linuxserver/fleet/v2/client/docker/queue/DockerImageUpdateResponse.java b/src/main/java/io/linuxserver/fleet/v2/client/docker/queue/DockerImageUpdateResponse.java index c4b337d..957c76b 100644 --- a/src/main/java/io/linuxserver/fleet/v2/client/docker/queue/DockerImageUpdateResponse.java +++ b/src/main/java/io/linuxserver/fleet/v2/client/docker/queue/DockerImageUpdateResponse.java @@ -41,7 +41,7 @@ public class DockerImageUpdateResponse implements AsyncDockerApiResponse { @Override public void handleDockerApiResponse() { - controller.getImageService().applyImageUpdate(imageKey, latestImage); + controller.getImageService().applyImageUpstreamUpdate(imageKey, latestImage); } @Override diff --git a/src/main/java/io/linuxserver/fleet/v2/db/DefaultImageDAO.java b/src/main/java/io/linuxserver/fleet/v2/db/DefaultImageDAO.java index 1611838..b86f252 100644 --- a/src/main/java/io/linuxserver/fleet/v2/db/DefaultImageDAO.java +++ b/src/main/java/io/linuxserver/fleet/v2/db/DefaultImageDAO.java @@ -28,12 +28,14 @@ import io.linuxserver.fleet.v2.types.*; import io.linuxserver.fleet.v2.types.internal.ImageOutlineRequest; import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest; import io.linuxserver.fleet.v2.types.internal.TagBranchOutlineRequest; +import io.linuxserver.fleet.v2.types.meta.ImageCoreMeta; import io.linuxserver.fleet.v2.types.meta.ImageMetaData; import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec; import io.linuxserver.fleet.v2.types.meta.history.ImagePullHistory; import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic; import io.linuxserver.fleet.v2.types.meta.template.ImageTemplateHolder; +import java.net.URL; import java.sql.*; import java.time.LocalDateTime; import java.util.ArrayList; @@ -50,7 +52,7 @@ public class DefaultImageDAO extends AbstractDAO implements ImageDAO { private static final String CreateRepositoryOutline = "{CALL Repository_CreateOutline(?,?,?,?,?,?,?,?)}"; private static final String StoreRepository = "{CALL Repository_Store(?,?,?,?)}"; - private static final String StoreImage = "{CALL Image_Store(?,?,?,?,?,?,?,?,?,?,?)}"; + private static final String StoreImage = "{CALL Image_Store(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)}"; private static final String CreateTagBranchOutline = "{CALL Image_CreateTagBranchOutline(?,?)}"; private static final String StoreTagBranch = "{CALL Image_StoreTagBranch(?,?,?,?)}"; private static final String StoreTagDigest = "{CALL Image_StoreTagDigest(?,?,?,?,?)}"; @@ -103,6 +105,12 @@ public class DefaultImageDAO extends AbstractDAO implements ImageDAO { call.setBoolean(i++, image.isSyncEnabled()); Utils.setNullableString(call, i++, image.getVersionMask()); + Utils.setNullableString(call, i++, image.getMetaData().getCategory()); + Utils.setNullableString(call, i++, image.getMetaData().getSupportUrl()); + Utils.setNullableString(call, i++, image.getMetaData().getAppUrl()); + Utils.setNullableString(call, i++, image.getMetaData().getBaseImage()); + Utils.setNullableString(call, i++, image.getMetaData().getAppImagePath()); + call.registerOutParameter(i, Types.VARCHAR); final ResultSet results = call.executeQuery(); @@ -486,7 +494,7 @@ public class DefaultImageDAO extends AbstractDAO implements ImageDAO { final Image image = new Image(imageKey, makeSyncSpec(results), - makeImageMetaData(connection, imageKey), + makeImageMetaData(connection, imageKey, results), makeCountData(results), results.getString("Description"), results.getTimestamp("LastUpdated").toLocalDateTime()); @@ -499,12 +507,22 @@ public class DefaultImageDAO extends AbstractDAO implements ImageDAO { return null; } - private ImageMetaData makeImageMetaData(final Connection connection, final ImageKey imageKey) throws SQLException { + private ImageMetaData makeImageMetaData(final Connection connection, final ImageKey imageKey, final ResultSet mainImageResults) throws SQLException { - return new ImageMetaData(makePullHistory(connection, imageKey), + return new ImageMetaData(makeCoreMeta(mainImageResults), + makePullHistory(connection, imageKey), templateFactory.makeTemplateHolder(connection, imageKey)); } + private ImageCoreMeta makeCoreMeta(final ResultSet mainImageResults) throws SQLException { + + return new ImageCoreMeta(mainImageResults.getString("CoreMetaImagePath"), + mainImageResults.getString("CoreMetaBaseImage"), + mainImageResults.getString("CoreMetaCategory"), + mainImageResults.getString("CoreMetaSupportUrl"), + mainImageResults.getString("CoreMetaAppUrl")); + } + private ImagePullHistory makePullHistory(final Connection connection, final ImageKey imageKey) throws SQLException { final ImagePullHistory pullHistory = new ImagePullHistory(); diff --git a/src/main/java/io/linuxserver/fleet/v2/service/ImageService.java b/src/main/java/io/linuxserver/fleet/v2/service/ImageService.java index 7ae8c8b..a0222db 100644 --- a/src/main/java/io/linuxserver/fleet/v2/service/ImageService.java +++ b/src/main/java/io/linuxserver/fleet/v2/service/ImageService.java @@ -28,14 +28,14 @@ import io.linuxserver.fleet.v2.service.util.TemplateMerger; import io.linuxserver.fleet.v2.types.*; import io.linuxserver.fleet.v2.types.docker.DockerImage; import io.linuxserver.fleet.v2.types.docker.DockerTag; -import io.linuxserver.fleet.v2.types.internal.ImageOutlineRequest; -import io.linuxserver.fleet.v2.types.internal.ImageTemplateRequest; -import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest; -import io.linuxserver.fleet.v2.types.internal.TagBranchOutlineRequest; +import io.linuxserver.fleet.v2.types.internal.*; +import io.linuxserver.fleet.v2.types.meta.ImageCoreMeta; 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; @@ -192,7 +192,7 @@ public class ImageService { return getAllRepositories().stream().filter(r -> !r.isHidden()).collect(Collectors.toList()); } - public Image applyImageUpdate(final ImageKey imageKey, final DockerImage latestImage) { + public Image applyImageUpstreamUpdate(final ImageKey imageKey, final DockerImage latestImage) { final Image cachedImage = findImage(imageKey); final Image cloned = cachedImage.cloneForUpdate(latestImage.getPullCount(), @@ -240,6 +240,25 @@ public class ImageService { storeImage(updatableClone); } + public void updateImageGeneralInfo(final ImageKey imageKey, final ImageGeneralInfoUpdateRequest generalInfoUpdateRequest) { + + final Image image = findImage(imageKey); + + String appLogoPath = image.getMetaData().getAppImagePath(); + if (null != generalInfoUpdateRequest.getImageAppLogo()) { + // TODO: Write FileManager + } + + final ImageCoreMeta coreMeta = new ImageCoreMeta(appLogoPath, + generalInfoUpdateRequest.getBaseImage(), + generalInfoUpdateRequest.getCategory(), + generalInfoUpdateRequest.getSupportUrl(), + generalInfoUpdateRequest.getApplicationUrl()); + + final Image cloned = image.cloneWithMetaData(image.getMetaData().cloneWithCoreMeta(coreMeta)); + storeImage(cloned); + } + public void updateImageTemplate(final ImageKey imageKey, final ImageTemplateRequest imageTemplateUpdateFields) { final Image image = findImage(imageKey); diff --git a/src/main/java/io/linuxserver/fleet/v2/service/SynchronisationService.java b/src/main/java/io/linuxserver/fleet/v2/service/SynchronisationService.java index faf1ae6..db3ad95 100644 --- a/src/main/java/io/linuxserver/fleet/v2/service/SynchronisationService.java +++ b/src/main/java/io/linuxserver/fleet/v2/service/SynchronisationService.java @@ -48,6 +48,8 @@ public class SynchronisationService extends AbstractAppService { if (repository.isSyncEnabled()) { + getLogger().info("synchroniseUpstreamRepository checking {} for new images since last sync", repository); + final List apiImages = getController().getConfiguredDockerDelegate().getImagesForRepository(repository.getKey()); for (DockerImage apiImage : apiImages) { diff --git a/src/main/java/io/linuxserver/fleet/v2/service/util/TemplateMerger.java b/src/main/java/io/linuxserver/fleet/v2/service/util/TemplateMerger.java index 5a0ab57..be8f8b0 100644 --- a/src/main/java/io/linuxserver/fleet/v2/service/util/TemplateMerger.java +++ b/src/main/java/io/linuxserver/fleet/v2/service/util/TemplateMerger.java @@ -31,11 +31,11 @@ public class TemplateMerger { public final Image mergeTemplateRequestIntoImage(final Image image, final ImageTemplateRequest templateRequest) { final ImageTemplateHolder templateHolder = makeTemplateHolder(templateRequest); - addMisc(templateRequest, templateHolder); - addPorts(templateRequest, templateHolder); - addVolumes(templateRequest, templateHolder); + addMisc( templateRequest, templateHolder); + addPorts( templateRequest, templateHolder); + addVolumes( templateRequest, templateHolder); addEnvironment(templateRequest, templateHolder); - addDevices(templateRequest, templateHolder); + addDevices( templateRequest, templateHolder); final Image cloned = image.cloneWithMetaData(image.getMetaData().cloneWithTemplate(templateHolder)); return cloned; diff --git a/src/main/java/io/linuxserver/fleet/v2/types/internal/AbstractParamRequest.java b/src/main/java/io/linuxserver/fleet/v2/types/internal/AbstractParamRequest.java new file mode 100644 index 0000000..37cccd5 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/v2/types/internal/AbstractParamRequest.java @@ -0,0 +1,92 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.v2.types.internal; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +public class AbstractParamRequest { + + private final Map> params; + + public AbstractParamRequest(final Map> params) { + this.params = params; + } + + protected final List getParams(final String key) { + return params.get(key); + } + + protected final String getOrNull(final String value) { + return "".equalsIgnoreCase(value.trim()) ? null : value; + } + + protected final String getFirstOrNull(final String key) { + + final List strings = params.get(key); + if (null == strings || strings.isEmpty()) { + return null; + } + return strings.get(0); + } + + protected final boolean getAsBoolean(final String value) { + return "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value); + } + + @SafeVarargs + protected final void checkLists(final List... lists) { + + boolean containsDifferent = false; + + boolean allNull = Stream.of(lists).allMatch(Objects::isNull); + boolean noneNull = Stream.of(lists).allMatch(Objects::nonNull); + + if (allNull || noneNull) { + + if (allNull) { + return; + } + + } else { + containsDifferent = true; + } + + if (!containsDifferent) { + + int prevSize = -1; + for (List list : lists) { + + if (prevSize != -1 && list.size() != prevSize) { + + containsDifferent = true; + break; + + } else { + prevSize = list.size(); + } + } + } + + if (containsDifferent) { + throw new IllegalArgumentException("One or more values are null when others are not, or sizes mismatch"); + } + } +} diff --git a/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageAppLogo.java b/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageAppLogo.java new file mode 100644 index 0000000..ced393d --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageAppLogo.java @@ -0,0 +1,64 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.v2.types.internal; + +import io.linuxserver.fleet.v2.key.ImageKey; + +import java.io.InputStream; + +public class ImageAppLogo { + + private final ImageKey imageKey; + private final InputStream rawDataStream; + private final String logoName; + private final long logoSize; + private final String fileExtension; + + public ImageAppLogo(final ImageKey imageKey, + final InputStream rawDataStream, + final String logoName, + final long logoSize, + final String fileExtension) { + + this.imageKey = imageKey; + this.rawDataStream = rawDataStream; + this.logoName = logoName; + this.logoSize = logoSize; + this.fileExtension = fileExtension; + } + + public final ImageKey getImageKey() { + return imageKey; + } + + public final InputStream getRawDataStream() { + return rawDataStream; + } + + public final String getLogoName() { + return logoName; + } + + public final long getLogoSize() { + return logoSize; + } + + public final String getFileExtension() { + return fileExtension; + } +} diff --git a/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageGeneralInfoUpdateRequest.java b/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageGeneralInfoUpdateRequest.java new file mode 100644 index 0000000..2612469 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageGeneralInfoUpdateRequest.java @@ -0,0 +1,52 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.v2.types.internal; + +import java.util.List; +import java.util.Map; + +public class ImageGeneralInfoUpdateRequest extends AbstractParamRequest { + + private final ImageAppLogo imageAppLogo; + + public ImageGeneralInfoUpdateRequest(final Map> params, + final ImageAppLogo imageAppLogo) { + super(params); + this.imageAppLogo = imageAppLogo; + } + + public final ImageAppLogo getImageAppLogo() { + return imageAppLogo; + } + + public final String getBaseImage() { + return getFirstOrNull("ImageBase"); + } + + public final String getCategory() { + return getFirstOrNull("ImageCategory"); + } + + public final String getSupportUrl() { + return getFirstOrNull("ImageSupportUrl"); + } + + public final String getApplicationUrl() { + return getFirstOrNull("ImageApplicationUrl"); + } +} diff --git a/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageTemplateRequest.java b/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageTemplateRequest.java index 0a1c3ea..8576b0b 100644 --- a/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageTemplateRequest.java +++ b/src/main/java/io/linuxserver/fleet/v2/types/internal/ImageTemplateRequest.java @@ -20,15 +20,11 @@ package io.linuxserver.fleet.v2.types.internal; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.stream.Stream; -public class ImageTemplateRequest { - - private final Map> rawTemplateParams; +public class ImageTemplateRequest extends AbstractParamRequest { public ImageTemplateRequest(final Map> rawTemplateParams) { - this.rawTemplateParams = rawTemplateParams; + super(rawTemplateParams); } public final String getRegistryUrl() { @@ -48,14 +44,14 @@ public class ImageTemplateRequest { } public final List getCapabilities() { - return rawTemplateParams.get("ImageTemplateCapabilities"); + return getParams("ImageTemplateCapabilities"); } public final List> getPorts() { - final List portNumbers = rawTemplateParams.get("imageTemplatePort"); - final List portProtocols = rawTemplateParams.get("imageTemplatePortProtocol"); - final List portDescriptions = rawTemplateParams.get("imageTemplatePortDescription"); + final List portNumbers = getParams("imageTemplatePort"); + final List portProtocols = getParams("imageTemplatePortProtocol"); + final List portDescriptions = getParams("imageTemplatePortDescription"); checkLists(portNumbers, portProtocols, portDescriptions); @@ -77,9 +73,9 @@ public class ImageTemplateRequest { public final List> getVolumes() { - final List volumeNames = rawTemplateParams.get("imageTemplateVolume"); - final List volumeReadOnlys = rawTemplateParams.get("imageTemplateVolumeReadonly"); - final List volumeDescriptions = rawTemplateParams.get("imageTemplateVolumeDescription"); + final List volumeNames = getParams("imageTemplateVolume"); + final List volumeReadOnlys = getParams("imageTemplateVolumeReadonly"); + final List volumeDescriptions = getParams("imageTemplateVolumeDescription"); checkLists(volumeNames, volumeReadOnlys, volumeDescriptions); @@ -101,8 +97,8 @@ public class ImageTemplateRequest { public final List> getEnvironment() { - final List envNames = rawTemplateParams.get("imageTemplateEnv"); - final List envDescriptions = rawTemplateParams.get("imageTemplateEnvDescription"); + final List envNames = getParams("imageTemplateEnv"); + final List envDescriptions = getParams("imageTemplateEnvDescription"); checkLists(envNames, envDescriptions); @@ -121,8 +117,8 @@ public class ImageTemplateRequest { public final List> getDevices() { - final List deviceNames = rawTemplateParams.get("imageTemplateDevice"); - final List deviceDescriptions = rawTemplateParams.get("imageTemplateDeviceDescription"); + final List deviceNames = getParams("imageTemplateDevice"); + final List deviceDescriptions = getParams("imageTemplateDeviceDescription"); checkLists(deviceNames, deviceDescriptions); @@ -139,62 +135,6 @@ public class ImageTemplateRequest { return env; } - private String getOrNull(final String value) { - return "".equalsIgnoreCase(value.trim()) ? null : value; - } - - private String getFirstOrNull(final String key) { - - final List strings = rawTemplateParams.get(key); - if (null == strings || strings.isEmpty()) { - return null; - } - return strings.get(0); - } - - private boolean getAsBoolean(final String value) { - return "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value); - } - - @SafeVarargs - private final void checkLists(final List... lists) { - - boolean containsDifferent = false; - - boolean allNull = Stream.of(lists).allMatch(Objects::isNull); - boolean noneNull = Stream.of(lists).allMatch(Objects::nonNull); - - if (allNull || noneNull) { - - if (allNull) { - return; - } - - } else { - containsDifferent = true; - } - - if (!containsDifferent) { - - int prevSize = -1; - for (List list : lists) { - - if (prevSize != -1 && list.size() != prevSize) { - - containsDifferent = true; - break; - - } else { - prevSize = list.size(); - } - } - } - - if (containsDifferent) { - throw new IllegalArgumentException("One or more values are null when others are not, or sizes mismatch"); - } - } - public static class TemplateItem { private final String name; diff --git a/src/main/java/io/linuxserver/fleet/v2/types/meta/ImageCoreMeta.java b/src/main/java/io/linuxserver/fleet/v2/types/meta/ImageCoreMeta.java new file mode 100644 index 0000000..d1717fa --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/v2/types/meta/ImageCoreMeta.java @@ -0,0 +1,62 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.v2.types.meta; + +import java.net.URL; + +public class ImageCoreMeta { + + private final String appImagePath; + private final String baseImage; + private final String category; + private final String supportUrl; + private final String appUrl; + + public ImageCoreMeta(final String appImagePath, + final String baseImage, + final String category, + final String supportUrl, + final String appUrl) { + + this.appImagePath = appImagePath; + this.baseImage = baseImage; + this.category = category; + this.supportUrl = supportUrl; + this.appUrl = appUrl; + } + + public final String getAppImagePath() { + return appImagePath; + } + + public final String getBaseImage() { + return baseImage; + } + + public final String getCategory() { + return category; + } + + public final String getSupportUrl() { + return supportUrl; + } + + public final String getAppUrl() { + return appUrl; + } +} diff --git a/src/main/java/io/linuxserver/fleet/v2/types/meta/ImageMetaData.java b/src/main/java/io/linuxserver/fleet/v2/types/meta/ImageMetaData.java index 3299e1b..94824d2 100644 --- a/src/main/java/io/linuxserver/fleet/v2/types/meta/ImageMetaData.java +++ b/src/main/java/io/linuxserver/fleet/v2/types/meta/ImageMetaData.java @@ -27,14 +27,22 @@ public class ImageMetaData { private final ImagePullHistory pullHistory; private final ImageTemplateHolder templateHolder; + private final ImageCoreMeta coreMeta; - public ImageMetaData(final ImagePullHistory pullHistory, final ImageTemplateHolder templateHolder) { + public ImageMetaData(final ImageCoreMeta coreMeta, + final ImagePullHistory pullHistory, + final ImageTemplateHolder templateHolder) { + this.coreMeta = coreMeta; this.pullHistory = pullHistory; this.templateHolder = templateHolder; } public final ImageMetaData cloneWithTemplate(final ImageTemplateHolder templateHolder) { - return new ImageMetaData(pullHistory, templateHolder); + return new ImageMetaData(getCoreMeta(), this.pullHistory, templateHolder); + } + + public final ImageMetaData cloneWithCoreMeta(final ImageCoreMeta coreMeta) { + return new ImageMetaData(coreMeta, this.pullHistory, getTemplates()); } public final List getHistoryFor(final ImagePullStatistic.StatGroupMode groupMode) { @@ -44,4 +52,37 @@ public class ImageMetaData { public final ImageTemplateHolder getTemplates() { return templateHolder; } + + public final ImageCoreMeta getCoreMeta() { + return coreMeta; + } + + public final String getAppImagePath() { + return getCoreMeta().getAppImagePath(); + } + + public final String getBaseImage() { + return getCoreMeta().getBaseImage(); + } + + public final String getCategory() { + return getCoreMeta().getCategory(); + } + + public final String getSupportUrl() { + return getCoreMeta().getSupportUrl(); + } + + public final String getAppUrl() { + return getCoreMeta().getAppUrl(); + } + + public final boolean isPopulated() { + + return ( + null != getCategory() || + null != getSupportUrl() || + null != getAppUrl() + ); + } } diff --git a/src/main/java/io/linuxserver/fleet/v2/web/routes/AdminImageEditController.java b/src/main/java/io/linuxserver/fleet/v2/web/routes/AdminImageEditController.java index 0279338..96aec16 100644 --- a/src/main/java/io/linuxserver/fleet/v2/web/routes/AdminImageEditController.java +++ b/src/main/java/io/linuxserver/fleet/v2/web/routes/AdminImageEditController.java @@ -18,10 +18,13 @@ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; +import io.javalin.http.UploadedFile; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.service.ImageService; import io.linuxserver.fleet.v2.types.docker.DockerCapability; +import io.linuxserver.fleet.v2.types.internal.ImageAppLogo; +import io.linuxserver.fleet.v2.types.internal.ImageGeneralInfoUpdateRequest; import io.linuxserver.fleet.v2.types.internal.ImageTemplateRequest; import io.linuxserver.fleet.v2.web.PageModelSpec; @@ -78,9 +81,31 @@ public class AdminImageEditController extends AbstractPageHandler { if (!ctx.isMultipartFormData()) { throw new IllegalArgumentException("Form submission must be form/multipart"); } + + imageService.updateImageGeneralInfo(imageKey, makeInfoRequest(imageKey, ctx)); } private void handleTemplateUpdate(final Context ctx, final ImageKey imageKey) { imageService.updateImageTemplate(imageKey, new ImageTemplateRequest(ctx.formParamMap())); } + + private ImageGeneralInfoUpdateRequest makeInfoRequest(final ImageKey imageKey, final Context ctx) { + + return new ImageGeneralInfoUpdateRequest(ctx.formParamMap(), + makeImageLogoIfPresent(imageKey, ctx.uploadedFile("ImageAppLogo"))); + } + + private ImageAppLogo makeImageLogoIfPresent(final ImageKey imageKey, final UploadedFile uploadedFile) { + + if (null != uploadedFile) { + + return new ImageAppLogo(imageKey, + uploadedFile.getContent(), + uploadedFile.getFilename(), + uploadedFile.getSize(), + uploadedFile.getExtension()); + } + + return null; + } } diff --git a/src/main/resources/db/migration/V2.3__UpdateImageViewForCoreMeta.sql b/src/main/resources/db/migration/V2.3__UpdateImageViewForCoreMeta.sql new file mode 100644 index 0000000..e0d5496 --- /dev/null +++ b/src/main/resources/db/migration/V2.3__UpdateImageViewForCoreMeta.sql @@ -0,0 +1,145 @@ +DELIMITER // + +CREATE OR REPLACE VIEW `Image_View` AS ( + + SELECT + + -- Key + images.`id` AS `ImageId`, + images.`name` AS `ImageName`, + images.`repository` AS `RepositoryId`, + repositories.`name` AS `RepositoryName`, + + -- Counts + images.`pulls` AS `LatestPullCount`, + images.`stars` AS `LatestStarCount`, + + -- Spec + images.`sync_enabled` AS `SyncEnabled`, + images.`version_mask` AS `VersionMask`, + images.`hidden` AS `Hidden`, + images.`stable` AS `Stable`, + images.`deprecated` AS `Deprecated`, + + -- General + images.`description` AS `Description`, + images.`modified` AS `LastUpdated`, + + -- Core Meta + meta.icon_url AS `CoreMetaImagePath`, + meta.base_image AS `CoreMetaBaseImage`, + meta.category AS `CoreMetaCategory`, + meta.support AS `CoreMetaSupportUrl`, + meta.app_url AS `CoreMetaAppUrl` + + FROM + Image images + JOIN + Repository repositories ON repositories.`id` = images.`repository` + LEFT JOIN + ImageMetadata meta on meta.`image_id` = images.`id` +); +// + +CREATE OR REPLACE PROCEDURE `Image_Store` +( + in_id INT, + in_pulls BIGINT, + in_stars INT, + in_description TEXT, + in_modified TIMESTAMP, + in_deprecated TINYINT, + in_hidden TINYINT, + in_stable TINYINT, + in_synchronised TINYINT, + in_version_mask VARCHAR(255), + in_category VARCHAR(255), + in_support VARCHAR(500), + in_app_url VARCHAR(500), + in_base_image VARCHAR(255), + in_icon_url VARCHAR(1000), + + OUT out_status enum('Updated', 'NoChange') +) +BEGIN + + IF NOT EXISTS(SELECT `id` FROM Image WHERE `id` = in_id) THEN + SET out_status = 'NoChange'; + ELSE + + UPDATE + Image + SET + `pulls` = in_pulls, + `stars` = in_stars, + `description` = in_description, + `modified` = in_modified, + `deprecated` = in_deprecated, + `hidden` = in_hidden, + `stable` = in_stable, + `sync_enabled` = in_synchronised, + `version_mask` = in_version_mask + WHERE + `id` = in_id; + + -- Only add core metadata if it has been provided with at least one value + IF + ( + in_category IS NOT NULL OR + in_support IS NOT NULL OR + in_app_url IS NOT NULL OR + in_base_image IS NOT NULL OR + in_icon_url IS NOT NULL + ) + THEN + + IF NOT EXISTS(SELECT 1 FROM ImageMetadata WHERE `image_id` = in_id) THEN + + INSERT INTO ImageMetadata + ( + `image_id`, + `category`, + `support`, + `app_url`, + `base_image`, + `icon_url` + ) + VALUES + ( + in_id, + in_category, + in_support, + in_app_url, + in_base_image, + in_icon_url + ); + + ELSE + + UPDATE ImageMetadata + SET + `category` = in_category, + `support` = in_support, + `app_url` = in_app_url, + `base_image` = in_base_image, + `icon_url` = in_icon_url + WHERE + `image_id` = in_id; + + END IF; + + END IF; + + IF ROW_COUNT() <> 1 THEN + SET out_status = 'NoChange'; + ELSE + SET out_status = 'Updated'; + END IF; + + CALL Image_StorePullHistory(in_id, in_pulls, out_status); + + SELECT * FROM ImageKey_View WHERE `ImageId` = in_id; + + END IF; + +END // diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties index 3a42abf..cae62fc 100644 --- a/src/main/resources/version.properties +++ b/src/main/resources/version.properties @@ -1,5 +1,5 @@ -#Sat Jan 18 12:56:52 GMT 2020 -app.build.date=2020-01-18T12\:56\:52 +#Sat Jan 18 17:28:00 GMT 2020 +app.build.date=2020-01-18T17\:28\:00 app.build.os=Linux app.build.user=josh app.version=2.0.0 diff --git a/src/main/resources/views/pages/admin/image-edit.ftl b/src/main/resources/views/pages/admin/image-edit.ftl index a1cc593..27e3556 100644 --- a/src/main/resources/views/pages/admin/image-edit.ftl +++ b/src/main/resources/views/pages/admin/image-edit.ftl @@ -23,6 +23,7 @@ <#import "../../ui/form/input.ftl" as input /> <#import "../../ui/elements/button.ftl" as button /> <#import "../../ui/elements/table.ftl" as table /> +<#import "../../ui/elements/tag.ftl" as tag /> <#import "../../ui/components/message.ftl" as message /> <#import "template-components/image-template-ports.ftl" as templatePorts /> @@ -71,21 +72,33 @@
- +
- <@input.text id="ImageBase" label="Base Image" isInline=true /> + + <@input.text id="ImageBase" label="Base Image" isInline=true value=image.metaData.baseImage + infoText="The name of the base image this image pulls from." /> +
- <@input.text id="ImageCategory" label="Category" isInline=true /> + + <@input.text id="ImageCategory" label="Category" isInline=true value=image.metaData.category + infoText="The application category for this image (e.g Home Automation)." /> +
- <@input.text id="ImageSupportUrl" label="Support Url" isInline=true /> + + <@input.text id="ImageSupportUrl" label="Support Url" isInline=true value=image.metaData.supportUrl + infoText="A link to the primary source of support for this image, such as a forum thread or documentation site." /> +
- <@input.text id="ImageAppUrl" label="Application Url" isInline=true /> + + <@input.text id="ImageApplicationUrl" label="Application Url" isInline=true value=image.metaData.appUrl + infoText="The primary URL for the application encapsulated by this image." /> +
@@ -132,16 +145,14 @@ <#if !tagBranch.branchProtected> <@button.buttons isRightAligned=true> - <@button.button extraClasses="remove-tag-branch" colour="danger" size="small"> - Remove + <@button.button extraClasses="remove-tag-branch" colour="white" size="small" title="Stop tracking this branch."> + <#else> - <@button.buttons isRightAligned=true> - <@button.button colour="light" size="small" isDisabled=true> - Protected - - +
+ <@tag.tag colour="light" value="Protected" extraAttributes='title="This branch can\'t be removed."' /> +
diff --git a/src/main/resources/views/pages/image.ftl b/src/main/resources/views/pages/image.ftl index ee88158..8d69336 100644 --- a/src/main/resources/views/pages/image.ftl +++ b/src/main/resources/views/pages/image.ftl @@ -95,7 +95,7 @@
-
+

Build Information

General build information for this image

@@ -109,6 +109,9 @@ <@table.halfDisplayRow title="Repository" value=image.repositoryName link="/?key=${image.repositoryKey}" /> <@table.halfDisplayRow title="Build Time" value=image.lastUpdatedAsString /> + <#if image.metaData.baseImage?has_content> + <@table.halfDisplayRow title="Base Image" value=image.metaData.baseImage?html /> + <@table.halfDisplayRow title="Synchronised" value=image.syncEnabled?string("Yes", "No") /> <@table.halfDisplayRow title="Stable" value=image.stable?string("Yes", "No") /> <@table.halfDisplayRow title="Deprecated" value=image.deprecated?string("Yes", "No") /> @@ -117,7 +120,35 @@
-
+ <#if image.metaData.populated> +
+ +

Support Information

+

External links and support

+ + <@table.table isFullWidth=true isNarrow=false isStriped=true isScrollable=true> + + + + + + + <#if image.metaData.category?has_content> + <@table.halfDisplayRow title="Category" value=image.metaData.category /> + + <#if image.metaData.appUrl?has_content> + <@table.halfDisplayRow title="Application Home" value=image.metaData.appUrl?html link=image.metaData.appUrl /> + + <#if image.metaData.supportUrl?has_content> + <@table.halfDisplayRow title="Support" value=image.metaData.supportUrl?html link=image.metaData.supportUrl /> + + + + +
+ + +

Tracked Tags

Known tags which link to a specific branched app version.

diff --git a/src/main/resources/views/prebuilt/docker-example.ftl b/src/main/resources/views/prebuilt/docker-example.ftl index 49450b3..38f886b 100644 --- a/src/main/resources/views/prebuilt/docker-example.ftl +++ b/src/main/resources/views/prebuilt/docker-example.ftl @@ -22,7 +22,10 @@ version: "2" services: ${containerName}: image: ${fullName}<#if latest?has_content>:${latest} - container_name: ${containerName}<#if templates.restartPolicy?has_content> + container_name: ${containerName} +<#if templates.hostNetworkingEnabled> network_mode: host + +<#if templates.restartPolicy?has_content> restart: ${templates.restartPolicy} <#if templates.capabilities?has_content> cap_add: <#list templates.capabilities as cap> @@ -39,7 +42,7 @@ services: - /host/path/to${volume.name}:${volume.name}<#if volume.readonly>:ro <#if volume.description?has_content># ${volume.description} -<#if templates.ports?has_content> ports: +<#if templates.ports?has_content && !templates.hostNetworkingEnabled> ports: <#list templates.ports as port> - ${port.name?string["##0"]}:${port.name?string["##0"]}/${port.protocol} <#if port.description?has_content># ${port.description} @@ -57,7 +60,8 @@ services:
docker create \
-  --name=${containerName} \<#if templates.env?has_content>
+  --name=${containerName} \<#if templates.hostNetworkingEnabled>
+  --net=host \<#if templates.env?has_content>
 <#list templates.env as env>
   -e ${env.name}=<#if env.description?has_content> `# ${env.description}` \
 
@@ -67,7 +71,7 @@ services:
   -v /host/path/to${volume.name}:${volume.name}<#if volume.readonly>:ro<#if volume.description?has_content> `# ${volume.description}` \
 
 
-<#if templates.ports?has_content>
+<#if templates.ports?has_content && !templates.hostNetworkingEnabled>
 <#list templates.ports as port>
   -p ${port.name?string["##0"]}:${port.name?string["##0"]}/${port.protocol}<#if port.description?has_content> `# ${port.description}` \
 
diff --git a/src/main/resources/views/ui/form/input.ftl b/src/main/resources/views/ui/form/input.ftl
index 4cf27a1..863b5ac 100644
--- a/src/main/resources/views/ui/form/input.ftl
+++ b/src/main/resources/views/ui/form/input.ftl
@@ -15,7 +15,7 @@ Reference: https://bulma.io/documentation/form/input/
     requiredHelp:String                     - If "required" is true, some help text to be displayed
     size:enum(small, normal, large)         - The size of the input
 -->
-<#macro input type id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" size="normal">
+<#macro input type id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" infoText="" size="normal">
 
     
<#if isInline> @@ -38,6 +38,9 @@ Reference: https://bulma.io/documentation/form/input/ <#if isRequired && requiredHelp?has_content>

${requiredHelp}

+ <#if infoText?has_content> +

${infoText}

+
<#if isInline>
@@ -64,8 +67,8 @@ Convenience macro to generate a text input requiredHelp:String - If "required" is true, some help text to be displayed size:enum(small, normal, large) - The size of the input --> -<#macro text id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" size="normal"> - <@input type="text" id=id value=value title=title label=label placeholder=placeholder extraAttributes=extraAttributes extraClasses=extraClasses icon=icon isRequired=isRequired isReadonly=isReadonly isDisabled=isDisabled isInvalid=isInvalid isInline=isInline requiredHelp=requiredHelp size=size /> +<#macro text id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" infoText="" size="normal"> + <@input type="text" id=id value=value title=title label=label placeholder=placeholder extraAttributes=extraAttributes extraClasses=extraClasses icon=icon isRequired=isRequired isReadonly=isReadonly isDisabled=isDisabled isInvalid=isInvalid isInline=isInline requiredHelp=requiredHelp infoText=infoText size=size /> <#--