diff --git a/build.gradle b/build.gradle index e98681d..ff8a2e2 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ repositories { mavenCentral() } -version = '1.4.0' +version = '1.4.1' sourceSets { diff --git a/src/main/java/io/linuxserver/fleet/cache/ImageCache.java b/src/main/java/io/linuxserver/fleet/cache/ImageCache.java index f6f15bf..0041212 100644 --- a/src/main/java/io/linuxserver/fleet/cache/ImageCache.java +++ b/src/main/java/io/linuxserver/fleet/cache/ImageCache.java @@ -18,20 +18,19 @@ package io.linuxserver.fleet.cache; import io.linuxserver.fleet.model.internal.Image; +import io.linuxserver.fleet.model.key.ImageKey; +import io.linuxserver.fleet.model.key.RepositoryKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; public class ImageCache { private static final Logger LOGGER = LoggerFactory.getLogger(ImageCache.class); - private final Map> cachedImages; + private final Map> cachedImages; public ImageCache() { this.cachedImages = new HashMap<>(); @@ -41,19 +40,19 @@ public class ImageCache { Image updatedImage = Image.copyOf(image); - if (cachedImages.containsKey(updatedImage.getRepositoryId())) { + if (cachedImages.containsKey(updatedImage.getKey().getRepositoryKey())) { - List images = cachedImages.get(updatedImage.getRepositoryId()); + List images = cachedImages.get(updatedImage.getKey().getRepositoryKey()); int cachedImageLocation = images.indexOf(updatedImage); if (cachedImageLocation > -1) { - LOGGER.info("updateCache({}) Updating existing cached image.", updatedImage.getName()); + LOGGER.info("updateCache({}) Updating existing cached image.", updatedImage); images.set(cachedImageLocation, updatedImage); } else { - LOGGER.info("updateCache({}) Adding image to cache", updatedImage.getName()); + LOGGER.info("updateCache({}) Adding image to cache", updatedImage); images.add(updatedImage); } @@ -62,33 +61,37 @@ public class ImageCache { List images = new ArrayList<>(); images.add(updatedImage); - LOGGER.info("updateCache({}:{}) Creating new cache for repository {}", updatedImage.getName(), updatedImage.getRepositoryId()); - cachedImages.put(updatedImage.getRepositoryId(), images); + LOGGER.info("updateCache({}) Creating new cache for repository {}", updatedImage, updatedImage.getRepositoryId()); + cachedImages.put(updatedImage.getKey().getRepositoryKey(), images); } } - public List getAll(int repositoryId) { + public List getAll(RepositoryKey repositoryKey) { - if (cachedImages.containsKey(repositoryId)) { - return cachedImages.get(repositoryId).stream().map(Image::copyOf).collect(Collectors.toList()); + if (cachedImages.containsKey(repositoryKey)) { + return cachedImages.get(repositoryKey).stream().map(Image::copyOf).collect(Collectors.toList()); } return new ArrayList<>(); } - public Image get(int repositoryId, String imageName) { + public Image get(ImageKey imageKey) { - if (cachedImages.containsKey(repositoryId)) { - return cachedImages.get(repositoryId).stream().filter(i -> i.getName().equals(imageName)).findFirst().orElse(null); + final RepositoryKey repositoryKey = imageKey.getRepositoryKey(); + + if (cachedImages.containsKey(repositoryKey)) { + return cachedImages.get(repositoryKey).stream().filter(i -> i.getName().equals(imageKey.getName())).findFirst().orElse(null); } return null; } - public void remove(Image image) { + public void remove(ImageKey imageKey) { - if (cachedImages.containsKey(image.getRepositoryId())) { - cachedImages.get(image.getRepositoryId()).remove(image); + final RepositoryKey repositoryKey = imageKey.getRepositoryKey(); + + if (cachedImages.containsKey(repositoryKey)) { + cachedImages.get(repositoryKey).removeIf(image -> image.getKey().equals(imageKey)); } } } diff --git a/src/main/java/io/linuxserver/fleet/core/FleetApp.java b/src/main/java/io/linuxserver/fleet/core/FleetApp.java index b35749b..fb37aab 100644 --- a/src/main/java/io/linuxserver/fleet/core/FleetApp.java +++ b/src/main/java/io/linuxserver/fleet/core/FleetApp.java @@ -131,7 +131,8 @@ public class FleetApp { path("/api/v1", () -> { get("/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()), new JsonTransformer()); - get("/pullHistory", new GetImagePullHistoryApi(beans.getImageDelegate()), new JsonTransformer()); + get("/image", new GetImageApi(beans.getImageDelegate()), new JsonTransformer()); + get("/pullHistory", new GetImagePullHistoryApi(beans.getImageDelegate()), new JsonTransformer()); after("/*", (request, response) -> { diff --git a/src/main/java/io/linuxserver/fleet/core/FleetBeans.java b/src/main/java/io/linuxserver/fleet/core/FleetBeans.java index 0199522..fb33103 100644 --- a/src/main/java/io/linuxserver/fleet/core/FleetBeans.java +++ b/src/main/java/io/linuxserver/fleet/core/FleetBeans.java @@ -27,7 +27,11 @@ import io.linuxserver.fleet.db.dao.DefaultUserDAO; import io.linuxserver.fleet.db.migration.DatabaseVersion; import io.linuxserver.fleet.delegate.*; import io.linuxserver.fleet.dockerhub.DockerHubV2Client; +import io.linuxserver.fleet.dockerhub.queue.DockerHubSyncConsumer; +import io.linuxserver.fleet.dockerhub.queue.DockerHubSyncQueue; import io.linuxserver.fleet.thread.TaskManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** *

@@ -36,6 +40,8 @@ import io.linuxserver.fleet.thread.TaskManager; */ public class FleetBeans { + private static final Logger LOGGER = LoggerFactory.getLogger(FleetBeans.class); + private final FleetProperties properties; private final ImageDelegate imageDelegate; private final RepositoryDelegate repositoryDelegate; @@ -46,6 +52,7 @@ public class FleetBeans { private final TaskDelegate taskDelegate; private final UserDelegate userDelegate; private final PasswordEncoder passwordEncoder; + private final DockerHubSyncQueue dockerHubSyncQueue; /** * Ensures the database is kept up to date. @@ -68,6 +75,14 @@ public class FleetBeans { userDelegate = new UserDelegate(passwordEncoder, new DefaultUserDAO(databaseConnection)); taskDelegate = new TaskDelegate(this); authenticationDelegate = new DefaultAuthenticationDelegate(AuthenticatorFactory.getAuthenticator(this)); + dockerHubSyncQueue = new DockerHubSyncQueue(); + + final int consumerThreadCount = properties.getQueueThreadCount(); + for (int i = 0; i < consumerThreadCount; i++) { + + LOGGER.info("Starting consumer thread " + i + "..."); + new DockerHubSyncConsumer(imageDelegate, dockerHubSyncQueue, "SyncThread-" + i).start(); + } } public FleetProperties getProperties() { diff --git a/src/main/java/io/linuxserver/fleet/core/FleetProperties.java b/src/main/java/io/linuxserver/fleet/core/FleetProperties.java index 6f5b2b4..8b38434 100644 --- a/src/main/java/io/linuxserver/fleet/core/FleetProperties.java +++ b/src/main/java/io/linuxserver/fleet/core/FleetProperties.java @@ -79,6 +79,10 @@ public class FleetProperties { return new DockerHubCredentials(username, password); } + public int getQueueThreadCount() { + return Integer.parseInt(getStringProperty("fleet.queue.threads")); + } + /** *

* Obtains the property value from three separate sources: first from the config file. If not present, it will look diff --git a/src/main/java/io/linuxserver/fleet/db/dao/DefaultImageDAO.java b/src/main/java/io/linuxserver/fleet/db/dao/DefaultImageDAO.java index 5ae5a3e..caeb904 100644 --- a/src/main/java/io/linuxserver/fleet/db/dao/DefaultImageDAO.java +++ b/src/main/java/io/linuxserver/fleet/db/dao/DefaultImageDAO.java @@ -24,6 +24,8 @@ import io.linuxserver.fleet.db.query.LimitedResult; import io.linuxserver.fleet.model.internal.Image; import io.linuxserver.fleet.model.internal.ImagePullStat; import io.linuxserver.fleet.model.internal.Tag; +import io.linuxserver.fleet.model.key.ImageKey; +import io.linuxserver.fleet.model.key.RepositoryKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,15 +47,15 @@ public class DefaultImageDAO implements ImageDAO { } @Override - public Image findImageByRepositoryAndImageName(int repositoryId, String imageName) { + public Image findImageByRepositoryAndImageName(ImageKey imageKey) { CallableStatement call = null; try (Connection connection = databaseConnection.getConnection()) { call = connection.prepareCall("{CALL Image_GetByName(?,?)}"); - call.setInt(1, repositoryId); - call.setString(2, imageName); + call.setInt(1, imageKey.getRepositoryKey().getId()); + call.setString(2, imageKey.getName()); ResultSet results = call.executeQuery(); if (results.next()) @@ -69,16 +71,16 @@ public class DefaultImageDAO implements ImageDAO { } @Override - public Image fetchImage(Integer id) { + public Image fetchImage(ImageKey imageKey) { - LOGGER.debug("Fetching image by ID: " + id); + LOGGER.debug("Fetching image by ID: " + imageKey); CallableStatement call = null; try (Connection connection = databaseConnection.getConnection()) { call = connection.prepareCall("{CALL Image_Get(?)}"); - call.setInt(1, id); + call.setInt(1, imageKey.getId()); ResultSet results = call.executeQuery(); if (results.next()) @@ -94,7 +96,7 @@ public class DefaultImageDAO implements ImageDAO { } @Override - public LimitedResult fetchImagesByRepository(int repositoryId) { + public LimitedResult fetchImagesByRepository(final RepositoryKey repositoryKey) { List images = new ArrayList<>(); @@ -103,7 +105,7 @@ public class DefaultImageDAO implements ImageDAO { try (Connection connection = databaseConnection.getConnection()) { call = connection.prepareCall("{CALL Image_GetAll(?,?)}"); - call.setInt(1, repositoryId); + call.setInt(1, repositoryKey.getId()); call.registerOutParameter(2, Types.INTEGER); ResultSet results = call.executeQuery(); @@ -130,7 +132,7 @@ public class DefaultImageDAO implements ImageDAO { try (Connection connection = databaseConnection.getConnection()) { call = connection.prepareCall("{CALL Image_Save(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); - setNullableInt(call, 1, image.getId()); + setNullableInt(call, 1, image.getKey().getId()); call.setInt(2, image.getRepositoryId()); call.setString(3, image.getName()); setNullableLong(call, 4, image.getPullCount()); @@ -154,7 +156,7 @@ public class DefaultImageDAO implements ImageDAO { String statusMessage = call.getString(15); if (InsertUpdateStatus.OK == status) - return new InsertUpdateResult<>(fetchImage(imageId), status, statusMessage); + return new InsertUpdateResult<>(fetchImage(image.getKey().cloneWithId(imageId)), status, statusMessage); return new InsertUpdateResult<>(status, statusMessage); @@ -169,14 +171,14 @@ public class DefaultImageDAO implements ImageDAO { } @Override - public void removeImage(Integer id) { + public void removeImage(final ImageKey imageKey) { CallableStatement call = null; try (Connection connection = databaseConnection.getConnection()) { call = connection.prepareCall("{CALL Image_Delete(?)}"); - call.setInt(1, id); + call.setInt(1, imageKey.getId()); call.executeUpdate(); call.close(); @@ -189,7 +191,7 @@ public class DefaultImageDAO implements ImageDAO { } @Override - public List fetchImagePullHistory(Integer imageId, ImagePullStat.GroupMode groupMode) { + public List fetchImagePullHistory(final ImageKey imageKey, ImagePullStat.GroupMode groupMode) { List pullHistory = new ArrayList<>(); @@ -198,7 +200,7 @@ public class DefaultImageDAO implements ImageDAO { try (Connection connection = databaseConnection.getConnection()) { call = connection.prepareCall("CALL Image_GetPullHistory(?, ?)"); - call.setInt(1, imageId); + call.setInt(1, imageKey.getId()); call.setString(2, groupMode.toString()); ResultSet results = call.executeQuery(); @@ -229,9 +231,14 @@ public class DefaultImageDAO implements ImageDAO { private Image parseImageFromResultSet(ResultSet results) throws SQLException { Image image = new Image( - results.getInt("ImageId"), - results.getInt("RepositoryId"), - results.getString("ImageName"), + new ImageKey( + results.getInt("ImageId"), + results.getString("ImageName"), + new RepositoryKey( + results.getInt("RepositoryId"), + results.getString("RepositoryName") + ) + ), new Tag( results.getString("LatestTagVersion"), results.getString("LatestMaskedTagVersion"), diff --git a/src/main/java/io/linuxserver/fleet/db/dao/DefaultRepositoryDAO.java b/src/main/java/io/linuxserver/fleet/db/dao/DefaultRepositoryDAO.java index 366fb68..eb46389 100644 --- a/src/main/java/io/linuxserver/fleet/db/dao/DefaultRepositoryDAO.java +++ b/src/main/java/io/linuxserver/fleet/db/dao/DefaultRepositoryDAO.java @@ -21,6 +21,7 @@ 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.internal.Repository; +import io.linuxserver.fleet.model.key.RepositoryKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,9 +29,7 @@ import java.sql.*; import java.util.ArrayList; import java.util.List; -import static io.linuxserver.fleet.db.dao.Utils.safeClose; -import static io.linuxserver.fleet.db.dao.Utils.setNullableInt; -import static io.linuxserver.fleet.db.dao.Utils.setNullableString; +import static io.linuxserver.fleet.db.dao.Utils.*; public class DefaultRepositoryDAO implements RepositoryDAO { @@ -43,14 +42,14 @@ public class DefaultRepositoryDAO implements RepositoryDAO { } @Override - public Repository fetchRepository(int id) { + public Repository fetchRepository(RepositoryKey repositoryKey) { CallableStatement call = null; try (Connection connection = databaseConnection.getConnection()) { call = connection.prepareCall("{CALL Repository_Get(?)}"); - call.setInt(1, id); + call.setInt(1, repositoryKey.getId()); ResultSet results = call.executeQuery(); if (results.next()) @@ -73,7 +72,7 @@ public class DefaultRepositoryDAO implements RepositoryDAO { try (Connection connection = databaseConnection.getConnection()) { call = connection.prepareCall("{CALL Repository_Save(?,?,?,?,?,?,?)}"); - setNullableInt(call, 1, repository.getId()); + setNullableInt(call, 1, repository.getKey().getId()); call.setString(2, repository.getName()); setNullableString(call, 3, repository.getVersionMask()); call.setBoolean(4, repository.isSyncEnabled()); @@ -89,7 +88,7 @@ public class DefaultRepositoryDAO implements RepositoryDAO { String statusMessage = call.getString(7); if (InsertUpdateStatus.OK == status) - return new InsertUpdateResult<>(fetchRepository(repositoryId), status, statusMessage); + return new InsertUpdateResult<>(fetchRepository(repository.getKey().cloneWithId(repositoryId)), status, statusMessage); return new InsertUpdateResult<>(status, statusMessage); @@ -171,7 +170,7 @@ public class DefaultRepositoryDAO implements RepositoryDAO { private Repository parseRepositoryFromResultSet(ResultSet results) throws SQLException { - Repository repository = new Repository(results.getInt("RepositoryId"), results.getString("RepositoryName")) + Repository repository = new Repository(new RepositoryKey(results.getInt("RepositoryId"), results.getString("RepositoryName"))) .withSyncEnabled(results.getBoolean("SyncEnabled")) .withVersionMask(results.getString("RepositoryVersionMask")) .withModifiedTime(results.getTimestamp("ModifiedTime").toLocalDateTime()); diff --git a/src/main/java/io/linuxserver/fleet/db/dao/DefaultUserDAO.java b/src/main/java/io/linuxserver/fleet/db/dao/DefaultUserDAO.java index 64e4136..32b7d19 100644 --- a/src/main/java/io/linuxserver/fleet/db/dao/DefaultUserDAO.java +++ b/src/main/java/io/linuxserver/fleet/db/dao/DefaultUserDAO.java @@ -85,7 +85,7 @@ public class DefaultUserDAO implements UserDAO { try (Connection connection = databaseConnection.getConnection()) { CallableStatement call = connection.prepareCall("{CALL User_Save(?,?,?,?,?,?)}"); - setNullableInt(call, 1, user.getId()); + setNullableInt(call, 1, user.getKey().getId()); call.setString(2, user.getUsername()); setNullableString(call, 3, user.getPassword()); @@ -137,7 +137,7 @@ public class DefaultUserDAO implements UserDAO { try (Connection connection = databaseConnection.getConnection()) { CallableStatement call = connection.prepareCall("{CALL User_Delete(?)}"); - call.setInt(1, user.getId()); + call.setInt(1, user.getKey().getId()); call.executeUpdate(); diff --git a/src/main/java/io/linuxserver/fleet/db/dao/ImageDAO.java b/src/main/java/io/linuxserver/fleet/db/dao/ImageDAO.java index 1125b86..efdd004 100644 --- a/src/main/java/io/linuxserver/fleet/db/dao/ImageDAO.java +++ b/src/main/java/io/linuxserver/fleet/db/dao/ImageDAO.java @@ -4,20 +4,22 @@ import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.db.query.LimitedResult; import io.linuxserver.fleet.model.internal.Image; import io.linuxserver.fleet.model.internal.ImagePullStat; +import io.linuxserver.fleet.model.key.ImageKey; +import io.linuxserver.fleet.model.key.RepositoryKey; import java.util.List; public interface ImageDAO { - Image findImageByRepositoryAndImageName(int repositoryId, String imageName); + Image findImageByRepositoryAndImageName(ImageKey imageKey); - Image fetchImage(Integer id); + Image fetchImage(ImageKey imageKey); - LimitedResult fetchImagesByRepository(int repositoryId); + LimitedResult fetchImagesByRepository(RepositoryKey repositoryKey); InsertUpdateResult saveImage(Image image); - void removeImage(Integer id); + void removeImage(ImageKey imageKey); - List fetchImagePullHistory(Integer imageId, ImagePullStat.GroupMode groupMode); + List fetchImagePullHistory(ImageKey imageKey, ImagePullStat.GroupMode groupMode); } diff --git a/src/main/java/io/linuxserver/fleet/db/dao/RepositoryDAO.java b/src/main/java/io/linuxserver/fleet/db/dao/RepositoryDAO.java index 512e07e..ca5cc56 100644 --- a/src/main/java/io/linuxserver/fleet/db/dao/RepositoryDAO.java +++ b/src/main/java/io/linuxserver/fleet/db/dao/RepositoryDAO.java @@ -2,12 +2,13 @@ package io.linuxserver.fleet.db.dao; import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.model.internal.Repository; +import io.linuxserver.fleet.model.key.RepositoryKey; import java.util.List; public interface RepositoryDAO { - Repository fetchRepository(int id); + Repository fetchRepository(RepositoryKey repositoryKey); InsertUpdateResult saveRepository(Repository repository); diff --git a/src/main/java/io/linuxserver/fleet/delegate/DockerHubDelegate.java b/src/main/java/io/linuxserver/fleet/delegate/DockerHubDelegate.java index 369cc6e..e03084b 100644 --- a/src/main/java/io/linuxserver/fleet/delegate/DockerHubDelegate.java +++ b/src/main/java/io/linuxserver/fleet/delegate/DockerHubDelegate.java @@ -63,6 +63,10 @@ public class DockerHubDelegate { return images; } + public DockerImage fetchImageFromRepository(String repositoryName, String imageName) { + return convertImage(dockerHubClient.fetchImageFromRepository(repositoryName, imageName)); + } + public List fetchAllTagsForImage(String repositoryName, String imageName) { return dockerHubClient.fetchAllTagsForImage(repositoryName, imageName).stream().map(this::convertTag).collect(Collectors.toList()); } @@ -80,6 +84,10 @@ public class DockerHubDelegate { private DockerImage convertImage(DockerHubV2Image dockerHubV2Image) { + if (dockerHubV2Image == null) { + return null; + } + return new DockerImage( dockerHubV2Image.getName(), dockerHubV2Image.getNamespace(), @@ -92,6 +100,10 @@ public class DockerHubDelegate { private DockerTag convertTag(DockerHubV2Tag dockerHubV2Tag) { + if (dockerHubV2Tag == null) { + return null; + } + return new DockerTag( dockerHubV2Tag.getName(), dockerHubV2Tag.getFullSize(), diff --git a/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java b/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java index e0a12ea..eb0ab75 100644 --- a/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java +++ b/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java @@ -24,6 +24,8 @@ import io.linuxserver.fleet.db.query.InsertUpdateStatus; import io.linuxserver.fleet.exception.SaveException; import io.linuxserver.fleet.model.internal.Image; import io.linuxserver.fleet.model.internal.ImagePullStat; +import io.linuxserver.fleet.model.key.ImageKey; +import io.linuxserver.fleet.model.key.RepositoryKey; import java.util.List; @@ -45,13 +47,13 @@ public class ImageDelegate { * already has the image stored against the repository. *

*/ - public Image findImageByRepositoryAndImageName(int repositoryId, String imageName) { + public Image findImageByRepositoryAndImageName(final ImageKey imageKey) { - Image cachedImage = imageCache.get(repositoryId, imageName); + Image cachedImage = imageCache.get(imageKey); if (null == cachedImage) { - Image image = imageDAO.findImageByRepositoryAndImageName(repositoryId, imageName); + Image image = imageDAO.findImageByRepositoryAndImageName(imageKey); if (null != image) { imageCache.updateCache(image); @@ -63,16 +65,30 @@ public class ImageDelegate { return cachedImage; } - public Image fetchImage(int id) { - return imageDAO.fetchImage(id); + public Image fetchImage(ImageKey imageKey) { + + Image cachedImage = imageCache.get(imageKey); + + if (null == cachedImage) { + + Image image = imageDAO.fetchImage(imageKey); + + if (null != image) { + imageCache.updateCache(image); + } + + return image; + } + + return cachedImage; } - public List fetchImagesByRepository(int repositoryId) { + public List fetchImagesByRepository(final RepositoryKey repositoryKey) { - List cachedImages = imageCache.getAll(repositoryId); + List cachedImages = imageCache.getAll(repositoryKey); if (cachedImages.isEmpty()) { - List images = imageDAO.fetchImagesByRepository(repositoryId).getResults(); + List images = imageDAO.fetchImagesByRepository(repositoryKey).getResults(); images.forEach(imageCache::updateCache); return images; @@ -81,13 +97,13 @@ public class ImageDelegate { return cachedImages; } - public void removeImage(Integer id) { + public void removeImage(ImageKey imageKey) { - Image existingImage = imageDAO.fetchImage(id); + Image existingImage = imageDAO.fetchImage(imageKey); if (null != existingImage) { - imageDAO.removeImage(id); - imageCache.remove(existingImage); + imageDAO.removeImage(imageKey); + imageCache.remove(imageKey); } } @@ -106,7 +122,7 @@ public class ImageDelegate { throw new SaveException(result.getStatusMessage()); } - public List fetchImagePullHistory(int id, ImagePullStat.GroupMode groupMode) { - return imageDAO.fetchImagePullHistory(id, groupMode); + public List fetchImagePullHistory(ImageKey imageKey, ImagePullStat.GroupMode groupMode) { + return imageDAO.fetchImagePullHistory(imageKey, groupMode); } } diff --git a/src/main/java/io/linuxserver/fleet/delegate/RepositoryDelegate.java b/src/main/java/io/linuxserver/fleet/delegate/RepositoryDelegate.java index d3971bb..e955c95 100644 --- a/src/main/java/io/linuxserver/fleet/delegate/RepositoryDelegate.java +++ b/src/main/java/io/linuxserver/fleet/delegate/RepositoryDelegate.java @@ -22,6 +22,7 @@ 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.internal.Repository; +import io.linuxserver.fleet.model.key.RepositoryKey; import java.util.List; @@ -33,8 +34,8 @@ public class RepositoryDelegate { this.repositoryDAO = repositoryDAO; } - public Repository fetchRepository(int id) { - return repositoryDAO.fetchRepository(id); + public Repository fetchRepository(RepositoryKey repositoryKey) { + return repositoryDAO.fetchRepository(repositoryKey); } public Repository saveRepository(Repository repository) throws SaveException { diff --git a/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncConsumer.java b/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncConsumer.java new file mode 100644 index 0000000..911ac11 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncConsumer.java @@ -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 . + */ + +package io.linuxserver.fleet.dockerhub.queue; + +import io.linuxserver.fleet.delegate.ImageDelegate; +import io.linuxserver.fleet.exception.SaveException; +import io.linuxserver.fleet.model.internal.Image; +import io.linuxserver.fleet.queue.AbstractQueueConsumer; +import io.linuxserver.fleet.queue.RequestQueue; + +public class DockerHubSyncConsumer extends AbstractQueueConsumer { + + private final ImageDelegate imageDelegate; + + public DockerHubSyncConsumer(ImageDelegate imageDelegate, RequestQueue requestQueue, String name) { + super(requestQueue, name); + this.imageDelegate = imageDelegate; + } + + @Override + protected void handleResponse(DockerHubSyncResponse response) { + + try { + + final Image image = response.getImage(); + imageDelegate.saveImage(image); + + } catch (SaveException e) { + getLogger().error("handleResponse unable to save image: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncQueue.java b/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncQueue.java new file mode 100644 index 0000000..54535e4 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncQueue.java @@ -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 . + */ + +package io.linuxserver.fleet.dockerhub.queue; + +import io.linuxserver.fleet.queue.AbstractRequestQueue; + +public class DockerHubSyncQueue extends AbstractRequestQueue { +} diff --git a/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncRequest.java b/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncRequest.java new file mode 100644 index 0000000..f1dc4f1 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncRequest.java @@ -0,0 +1,55 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.dockerhub.queue; + +import io.linuxserver.fleet.delegate.DockerHubDelegate; +import io.linuxserver.fleet.model.internal.Image; +import io.linuxserver.fleet.queue.FleetRequest; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class DockerHubSyncRequest implements FleetRequest { + + private final DockerHubDelegate dockerHubDelegate; + private final Image image; + + public DockerHubSyncRequest(DockerHubDelegate dockerHubDelegate, Image image) { + + this.dockerHubDelegate = dockerHubDelegate; + this.image = image; + } + + @Override + public DockerHubSyncResponse execute() { + return new DockerHubSyncResponse(dockerHubDelegate, image); + } + + @Override + public boolean equals(Object o) { + + if (!(o instanceof DockerHubSyncRequest)) + return false; + + return EqualsBuilder.reflectionEquals(this, "dockerHubDelegate"); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this, "dockerHubDelegate"); + } +} diff --git a/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncResponse.java b/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncResponse.java new file mode 100644 index 0000000..9b74520 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/dockerhub/queue/DockerHubSyncResponse.java @@ -0,0 +1,100 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.dockerhub.queue; + +import io.linuxserver.fleet.delegate.DockerHubDelegate; +import io.linuxserver.fleet.model.docker.DockerImage; +import io.linuxserver.fleet.model.docker.DockerTag; +import io.linuxserver.fleet.model.internal.Image; +import io.linuxserver.fleet.model.internal.Tag; +import io.linuxserver.fleet.queue.FleetResponse; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DockerHubSyncResponse implements FleetResponse { + + private final DockerHubDelegate dockerHubDelegate; + private final Image image; + + public DockerHubSyncResponse(DockerHubDelegate dockerHubDelegate, Image image) { + + this.dockerHubDelegate = dockerHubDelegate; + this.image = Image.copyOf(image); + } + + @Override + public void handle() { + + DockerImage dockerImage = dockerHubDelegate.fetchImageFromRepository(image.getKey().getRepositoryKey().getName(), image.getName()); + + String versionMask = getVersionMask(null, image.getVersionMask()); + Tag maskedVersion = getLatestTagAndCreateMaskedVersion(versionMask); + + image.withPullCount(dockerImage.getPullCount()); + image.updateTag(maskedVersion); + } + + public Image getImage() { + return image; + } + + private String getVersionMask(String repositoryMask, String imageMask) { + return imageMask == null ? repositoryMask : imageMask; + } + + private Tag getLatestTagAndCreateMaskedVersion(String versionMask) { + + DockerTag tag = dockerHubDelegate.fetchLatestImageTag(image.getKey().getRepositoryKey().getName(), image.getName()); + + if (null == tag) + return Tag.NONE; + + if (isTagJustLatestAndNotAVersion(tag) || null == versionMask) + return new Tag(tag.getName(), tag.getName(), tag.getBuildDate()); + + return new Tag(tag.getName(), extractMaskedVersion(tag.getName(), versionMask), tag.getBuildDate()); + } + + 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; + } + + /** + *

+ * If the top-level tag is not versioned, a mask can't be applied. + *

+ */ + private boolean isTagJustLatestAndNotAVersion(DockerTag tag) { + return "latest".equals(tag.getName()); + } +} diff --git a/src/main/java/io/linuxserver/fleet/model/api/ApiImage.java b/src/main/java/io/linuxserver/fleet/model/api/ApiImage.java index cf250d1..aa17a8f 100644 --- a/src/main/java/io/linuxserver/fleet/model/api/ApiImage.java +++ b/src/main/java/io/linuxserver/fleet/model/api/ApiImage.java @@ -21,6 +21,7 @@ import io.linuxserver.fleet.model.internal.Image; public class ApiImage { + private String imageKey; private String name; private long pullCount; private String version; @@ -28,6 +29,10 @@ public class ApiImage { private boolean deprecated; private String buildDate; + public String getImageKey() { + return imageKey; + } + public String getName() { return name; } @@ -56,6 +61,7 @@ public class ApiImage { ApiImage apiImage = new ApiImage(); + apiImage.imageKey = image.getKey().toString(); apiImage.name = image.getName(); apiImage.pullCount = image.getPullCount(); apiImage.version = image.getMaskedVersion(); diff --git a/src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java b/src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java index ba33c9d..a5a98d9 100644 --- a/src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java +++ b/src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java @@ -49,7 +49,7 @@ public class ApiImagePullHistory { public static ApiImagePullHistory fromPullStats(Image image, List stats) { ApiImagePullHistory history = new ApiImagePullHistory(); - history.imageId = image.getId(); + history.imageId = image.getKey().getId(); history.imageName = image.getName(); history.groupMode = stats.get(0).getGroupMode().toString(); diff --git a/src/main/java/io/linuxserver/fleet/model/internal/Image.java b/src/main/java/io/linuxserver/fleet/model/internal/Image.java index 83e987c..9ad1c24 100644 --- a/src/main/java/io/linuxserver/fleet/model/internal/Image.java +++ b/src/main/java/io/linuxserver/fleet/model/internal/Image.java @@ -17,9 +17,11 @@ package io.linuxserver.fleet.model.internal; +import io.linuxserver.fleet.model.key.AbstractHasKey; +import io.linuxserver.fleet.model.key.ImageKey; + import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.Objects; /** *

@@ -27,42 +29,42 @@ import java.util.Objects; * specific information regarding its build status and pull count. *

*/ -public class Image extends PersistableItem { +public class Image extends AbstractHasKey { - private final int repositoryId; - private final String name; + private static final DateTimeFormatter DATE_PATTERN = DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss"); - private Tag tag; - private long pullCount; - private String versionMask; - private boolean unstable; - private boolean hidden; + private Tag tag; + private long pullCount; + private String versionMask; + private boolean unstable; + private boolean hidden; + private LocalDateTime modifiedTime; + private boolean deprecated; + private String deprecationReason; - private boolean deprecated; - private String deprecationReason; - - public Image(Integer id, int repositoryId, String name, Tag latestVersion) { - - super(id); - - this.name = name; - this.repositoryId = repositoryId; - this.tag = new Tag(latestVersion.getVersion(), latestVersion.getMaskedVersion(), latestVersion.getBuildDate()); + public Image(final ImageKey imageKey, final Tag latestVersion) { + super(imageKey); + this.tag = new Tag(latestVersion.getVersion(), latestVersion.getMaskedVersion(), latestVersion.getBuildDate()); } - public Image(int repositoryId, String name) { - this(null, repositoryId, name, Tag.NONE); + public Image(final ImageKey imageKey) { + this(imageKey, Tag.NONE); + } + + public static Image makeFromKey(ImageKey imageKey) { + return new Image(imageKey); } public static Image copyOf(Image image) { - Image cloned = new Image(image.getId(), image.repositoryId, image.name, image.tag); + Image cloned = new Image(image.getKey(), image.tag); cloned.pullCount = image.pullCount; cloned.versionMask = image.versionMask; cloned.unstable = image.unstable; cloned.hidden = image.hidden; cloned.deprecated = image.deprecated; cloned.deprecationReason = image.deprecationReason; + cloned.modifiedTime = image.modifiedTime; return cloned; } @@ -103,16 +105,22 @@ public class Image extends PersistableItem { return this; } + public Image withModifiedTime(final LocalDateTime modifiedTime) { + + this.modifiedTime = modifiedTime; + return this; + } + public void updateTag(Tag maskedVersion) { this.tag = new Tag(maskedVersion.getVersion(), maskedVersion.getMaskedVersion(), maskedVersion.getBuildDate()); } public int getRepositoryId() { - return repositoryId; + return getKey().getRepositoryKey().getId(); } public String getName() { - return name; + return getKey().getName(); } public long getPullCount() { @@ -134,7 +142,7 @@ public class Image extends PersistableItem { public String getBuildDateAsString() { if (getBuildDate() != null) { - return getBuildDate().format(DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss")); + return getBuildDate().format(DATE_PATTERN); } return null; @@ -160,17 +168,20 @@ public class Image extends PersistableItem { return deprecationReason; } - @Override - public boolean equals(Object o) { + public LocalDateTime getModifiedTime() { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Image image = (Image) o; - return repositoryId == image.repositoryId && Objects.equals(name, image.name); - } + if (null != modifiedTime) { - @Override - public int hashCode() { - return Objects.hash(repositoryId, name); + return LocalDateTime.of( + modifiedTime.getYear(), + modifiedTime.getMonth(), + modifiedTime.getDayOfMonth(), + modifiedTime.getHour(), + modifiedTime.getMinute(), + modifiedTime.getSecond() + ); + } + + return null; } } diff --git a/src/main/java/io/linuxserver/fleet/model/internal/Repository.java b/src/main/java/io/linuxserver/fleet/model/internal/Repository.java index 6289df2..e3ff9e5 100644 --- a/src/main/java/io/linuxserver/fleet/model/internal/Repository.java +++ b/src/main/java/io/linuxserver/fleet/model/internal/Repository.java @@ -17,23 +17,23 @@ package io.linuxserver.fleet.model.internal; -public class Repository extends PersistableItem { +import io.linuxserver.fleet.model.key.AbstractHasKey; +import io.linuxserver.fleet.model.key.RepositoryKey; - private final String name; +import java.time.LocalDateTime; - private String versionMask; - private boolean syncEnabled; +public class Repository extends AbstractHasKey { - public Repository(Integer id, String name) { - super(id); + private String versionMask; + private boolean syncEnabled; + private LocalDateTime modifiedTime; - this.name = name; + public Repository(final RepositoryKey repositoryKey) { + super(repositoryKey); } - public Repository(String name) { - super(); - - this.name = name; + public static Repository copyOf(Repository repository) { + return new Repository(repository.getKey()).withVersionMask(repository.versionMask).withSyncEnabled(repository.syncEnabled); } public Repository withVersionMask(String versionMask) { @@ -48,8 +48,14 @@ public class Repository extends PersistableItem { return this; } + public Repository withModifiedTime(LocalDateTime modifiedTime) { + + this.modifiedTime = modifiedTime; + return this; + } + public String getName() { - return name; + return getKey().getName(); } public String getVersionMask() { @@ -59,4 +65,21 @@ public class Repository extends PersistableItem { public boolean isSyncEnabled() { return syncEnabled; } + + public LocalDateTime getModifiedTime() { + + if (null != modifiedTime) { + + return LocalDateTime.of( + modifiedTime.getYear(), + modifiedTime.getMonth(), + modifiedTime.getDayOfMonth(), + modifiedTime.getHour(), + modifiedTime.getMinute(), + modifiedTime.getSecond() + ); + } + + return null; + } } diff --git a/src/main/java/io/linuxserver/fleet/model/internal/User.java b/src/main/java/io/linuxserver/fleet/model/internal/User.java index 026d127..1af541f 100644 --- a/src/main/java/io/linuxserver/fleet/model/internal/User.java +++ b/src/main/java/io/linuxserver/fleet/model/internal/User.java @@ -17,20 +17,27 @@ package io.linuxserver.fleet.model.internal; -public class User extends PersistableItem { +import io.linuxserver.fleet.model.key.AbstractHasKey; +import io.linuxserver.fleet.model.key.UserKey; - private final String username; - private final String password; +import java.time.LocalDateTime; + +public class User extends AbstractHasKey { + + private final String username; + private final String password; + + private LocalDateTime modifiedTime; public User(String username, String password) { - super(); + super(new UserKey()); this.username = username; this.password = password; } public User(Integer id, String username, String password) { - super(id); + super(new UserKey(id)); this.username = username; this.password = password; @@ -43,4 +50,27 @@ public class User extends PersistableItem { public String getPassword() { return password; } + + public User withModifiedTime(LocalDateTime modifiedTime) { + + this.modifiedTime = modifiedTime; + return this; + } + + public LocalDateTime getModifiedTime() { + + if (null != modifiedTime) { + + return LocalDateTime.of( + modifiedTime.getYear(), + modifiedTime.getMonth(), + modifiedTime.getDayOfMonth(), + modifiedTime.getHour(), + modifiedTime.getMinute(), + modifiedTime.getSecond() + ); + } + + return null; + } } diff --git a/src/main/java/io/linuxserver/fleet/model/key/AbstractHasKey.java b/src/main/java/io/linuxserver/fleet/model/key/AbstractHasKey.java new file mode 100644 index 0000000..db52b59 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/model/key/AbstractHasKey.java @@ -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 . + */ + +package io.linuxserver.fleet.model.key; + +public abstract class AbstractHasKey implements HasKey { + + private final KEY key; + + public AbstractHasKey(final KEY key) { + this.key = key; + } + + @Override + public final KEY getKey() { + return key; + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public boolean equals(Object o) { + + if (!(o instanceof HasKey)) { + return false; + } + + return key.equals(((HasKey) o).getKey()); + } + + @Override + public final String toString() { + return key.toString(); + } +} diff --git a/src/main/java/io/linuxserver/fleet/model/internal/PersistableItem.java b/src/main/java/io/linuxserver/fleet/model/key/AbstractKey.java similarity index 52% rename from src/main/java/io/linuxserver/fleet/model/internal/PersistableItem.java rename to src/main/java/io/linuxserver/fleet/model/key/AbstractKey.java index c92f510..21cdcf4 100644 --- a/src/main/java/io/linuxserver/fleet/model/internal/PersistableItem.java +++ b/src/main/java/io/linuxserver/fleet/model/key/AbstractKey.java @@ -15,41 +15,47 @@ * along with this program. If not, see . */ -package io.linuxserver.fleet.model.internal; +package io.linuxserver.fleet.model.key; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; +public abstract class AbstractKey implements Key { -import java.time.LocalDateTime; + private final Integer id; -public abstract class PersistableItem { - - private Integer id; - private LocalDateTime modifiedTime; - - PersistableItem() { } - - PersistableItem(Integer id) { + public AbstractKey(final Integer id) { this.id = id; } - @SuppressWarnings("unchecked") - public T withModifiedTime(LocalDateTime modifiedTime) { - - this.modifiedTime = modifiedTime; - return (T) this; - } - - public Integer getId() { + @Override + public final Integer getId() { return id; } - public LocalDateTime getModifiedTime() { - return modifiedTime; + @Override + public boolean equals(Object o) { + + if (!(o instanceof Key)) { + return false; + } + + if (null == id) { + return ((Key) o).getId() == null; + } + + return ((Key) o).getId().equals(id); + } + + @Override + public int hashCode() { + + if (null == id) { + return -1; + } + + return id.hashCode(); } @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + return null == id ? "" : String.valueOf(id); } } diff --git a/src/main/java/io/linuxserver/fleet/model/key/HasKey.java b/src/main/java/io/linuxserver/fleet/model/key/HasKey.java new file mode 100644 index 0000000..8ef0801 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/model/key/HasKey.java @@ -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 . + */ + +package io.linuxserver.fleet.model.key; + +public interface HasKey { + + KEY getKey(); +} diff --git a/src/main/java/io/linuxserver/fleet/model/key/ImageKey.java b/src/main/java/io/linuxserver/fleet/model/key/ImageKey.java new file mode 100644 index 0000000..2278b4c --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/model/key/ImageKey.java @@ -0,0 +1,72 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.model.key; + +public class ImageKey extends AbstractKey { + + private static final String KeyPattern = "^\\d+:\\d+:[^/]+/[^/]+$"; + + private final RepositoryKey repositoryKey; + private final String name; + + public ImageKey(final String name, final RepositoryKey repositoryKey) { + this(null, name, repositoryKey); + } + + public ImageKey(final Integer id, final String name, final RepositoryKey repositoryKey) { + super(id); + + this.repositoryKey = repositoryKey; + this.name = name; + } + + public static ImageKey parse(final String keyAsString) { + + if (keyAsString.matches(KeyPattern)) { + + final String[] keyParts = keyAsString.split(":"); + final String[] names = keyParts[2].split("/"); + final int repositoryId = Integer.parseInt(keyParts[0]); + final int imageId = Integer.parseInt(keyParts[1]); + final String repositoryName = names[0]; + final String imageName = names[1]; + + return new ImageKey(imageId, imageName, new RepositoryKey(repositoryId, repositoryName)); + + } else { + throw new IllegalArgumentException("Key pattern is malformed"); + } + } + + public ImageKey cloneWithId(int id) { + return new ImageKey(id, name, repositoryKey); + } + + public final RepositoryKey getRepositoryKey() { + return repositoryKey; + } + + public final String getName() { + return name; + } + + @Override + public String toString() { + return repositoryKey.getId() + ":" + super.toString() + ":" + repositoryKey.getName() + "/" + name; + } +} diff --git a/src/main/java/io/linuxserver/fleet/model/key/Key.java b/src/main/java/io/linuxserver/fleet/model/key/Key.java new file mode 100644 index 0000000..5311e6d --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/model/key/Key.java @@ -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 . + */ + +package io.linuxserver.fleet.model.key; + +public interface Key { + + Integer getId(); +} diff --git a/src/main/java/io/linuxserver/fleet/model/key/RepositoryKey.java b/src/main/java/io/linuxserver/fleet/model/key/RepositoryKey.java new file mode 100644 index 0000000..20d8e64 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/model/key/RepositoryKey.java @@ -0,0 +1,49 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.model.key; + +public class RepositoryKey extends AbstractKey { + + private final String name; + + public RepositoryKey(final int id) { + this(id, ""); + } + + public RepositoryKey(final String name) { + this(null, name); + } + + public RepositoryKey(final Integer id, final String name) { + super(id); + this.name = name; + } + + public RepositoryKey cloneWithId(int id) { + return new RepositoryKey(id, name); + } + + public final String getName() { + return name; + } + + @Override + public String toString() { + return super.toString() + ":" + name; + } +} diff --git a/src/main/java/io/linuxserver/fleet/model/key/UserKey.java b/src/main/java/io/linuxserver/fleet/model/key/UserKey.java new file mode 100644 index 0000000..30bea6c --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/model/key/UserKey.java @@ -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 . + */ + +package io.linuxserver.fleet.model.key; + +public class UserKey extends AbstractKey { + + public UserKey() { + this(null); + } + + public UserKey(Integer id) { + super(id); + } +} diff --git a/src/main/java/io/linuxserver/fleet/queue/AbstractQueueConsumer.java b/src/main/java/io/linuxserver/fleet/queue/AbstractQueueConsumer.java new file mode 100644 index 0000000..06c76cc --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/queue/AbstractQueueConsumer.java @@ -0,0 +1,87 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.queue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.atomic.AtomicReference; + +public abstract class AbstractQueueConsumer> extends Thread implements QueueConsumer { + + private final Logger LOGGER = LoggerFactory.getLogger(getClass().getName()); + + private final RequestQueue requestQueue; + private final AtomicReference state; + + public AbstractQueueConsumer(final RequestQueue requestQueue, final String name) { + super(name + "-Consumer"); + this.requestQueue = requestQueue; + this.state = new AtomicReference<>(ConsumerState.Stopped); + } + + @Override + public void run() { + + while (isRunning()) { + consume(); + } + } + + @Override + public void start() { + + getLogger().info("Starting..."); + super.start(); + state.set(ConsumerState.Running); + } + + @Override + public void consume() { + + try { + + T request = requestQueue.takeOneRequest(); + R response = request.execute(); + + handleResponse(response); + + } catch (Exception e) { + getLogger().error("consume caught unhandled exception", e); + } + } + + public boolean isRunning() { + return state.get().isRunning(); + } + + protected abstract void handleResponse(R response); + + public Logger getLogger() { + return LOGGER; + } + + private enum ConsumerState { + + Running, Stopped; + + final boolean isRunning() { + return this == Running; + } + } +} diff --git a/src/main/java/io/linuxserver/fleet/queue/AbstractRequestQueue.java b/src/main/java/io/linuxserver/fleet/queue/AbstractRequestQueue.java new file mode 100644 index 0000000..3392685 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/queue/AbstractRequestQueue.java @@ -0,0 +1,64 @@ +/* + * 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 . + */ + +package io.linuxserver.fleet.queue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +public class AbstractRequestQueue> implements RequestQueue { + + private final Logger LOGGER = LoggerFactory.getLogger(getClass().getName()); + + private final BlockingQueue requestQueue; + + public AbstractRequestQueue() { + this.requestQueue = new LinkedBlockingDeque<>(); + } + + @Override + public synchronized void enqueueRequest(final T request) { + + try { + + if (!requestQueue.contains(request)) { + + final boolean added = requestQueue.add(request); + if (!added) { + LOGGER.warn("enqueueRequest unable to add request to queue"); + } + } + + } catch (Exception e) { + LOGGER.error("enqueueRequest caught unhandled exception {}", e.getMessage()); + } + } + + @Override + public synchronized T takeOneRequest() { + + try { + return requestQueue.take(); + } catch (InterruptedException e) { + LOGGER.error("takeOneRequest was interrupted: {}", e.getMessage()); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/io/linuxserver/fleet/queue/FleetRequest.java b/src/main/java/io/linuxserver/fleet/queue/FleetRequest.java new file mode 100644 index 0000000..5e3616d --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/queue/FleetRequest.java @@ -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 . + */ + +package io.linuxserver.fleet.queue; + +public interface FleetRequest { + + T execute(); +} diff --git a/src/main/java/io/linuxserver/fleet/queue/FleetResponse.java b/src/main/java/io/linuxserver/fleet/queue/FleetResponse.java new file mode 100644 index 0000000..91a12bd --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/queue/FleetResponse.java @@ -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 . + */ + +package io.linuxserver.fleet.queue; + +public interface FleetResponse { + + void handle(); +} diff --git a/src/main/java/io/linuxserver/fleet/queue/QueueConsumer.java b/src/main/java/io/linuxserver/fleet/queue/QueueConsumer.java new file mode 100644 index 0000000..d49a187 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/queue/QueueConsumer.java @@ -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 . + */ + +package io.linuxserver.fleet.queue; + +public interface QueueConsumer> { + + void consume(); +} diff --git a/src/main/java/io/linuxserver/fleet/queue/RequestQueue.java b/src/main/java/io/linuxserver/fleet/queue/RequestQueue.java new file mode 100644 index 0000000..14c4ba0 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/queue/RequestQueue.java @@ -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 . + */ + +package io.linuxserver.fleet.queue; + +public interface RequestQueue { + + void enqueueRequest(T request); + + T takeOneRequest(); +} diff --git a/src/main/java/io/linuxserver/fleet/sync/DefaultSynchronisationState.java b/src/main/java/io/linuxserver/fleet/sync/DefaultSynchronisationState.java index 6c713b0..694f778 100644 --- a/src/main/java/io/linuxserver/fleet/sync/DefaultSynchronisationState.java +++ b/src/main/java/io/linuxserver/fleet/sync/DefaultSynchronisationState.java @@ -24,6 +24,8 @@ import io.linuxserver.fleet.model.docker.DockerTag; import io.linuxserver.fleet.model.internal.Image; import io.linuxserver.fleet.model.internal.Repository; import io.linuxserver.fleet.model.internal.Tag; +import io.linuxserver.fleet.model.key.ImageKey; +import io.linuxserver.fleet.model.key.RepositoryKey; import io.linuxserver.fleet.sync.event.ImageUpdateEvent; import io.linuxserver.fleet.sync.event.RepositoriesScannedEvent; import org.slf4j.Logger; @@ -137,7 +139,7 @@ public class DefaultSynchronisationState implements SynchronisationState { try { DockerImage dockerImage = images.get(i); - Image image = configureImage(repository.getId(), dockerImage, context); + Image image = configureImage(repository, dockerImage, context); String versionMask = getVersionMask(repository.getVersionMask(), image.getVersionMask()); Tag maskedVersion = getLatestTagAndCreateMaskedVersion(repository.getName(), image.getName(), versionMask, context); @@ -167,7 +169,7 @@ public class DefaultSynchronisationState implements SynchronisationState { if (!repositories.contains(storedRepository.getName())) { LOGGER.info("Found repository which no longer exists in Docker Hub. Removing {}", storedRepository.getName()); - context.getRepositoryDelegate().removeRepository(storedRepository.getId()); + context.getRepositoryDelegate().removeRepository(storedRepository.getKey().getId()); } } } @@ -177,12 +179,12 @@ public class DefaultSynchronisationState implements SynchronisationState { List dockerHubImageNames = images.stream().map(DockerImage::getName).collect(Collectors.toList()); LOGGER.info("Checking for any removed images under {}", repository.getName()); - for (Image storedImage : context.getImageDelegate().fetchImagesByRepository(repository.getId())) { + for (Image storedImage : context.getImageDelegate().fetchImagesByRepository(repository.getKey())) { if (!dockerHubImageNames.contains(storedImage.getName())) { LOGGER.info("Found image which no longer exists in Docker Hub. Removing {}", storedImage.getName()); - context.getImageDelegate().removeImage(storedImage.getId()); + context.getImageDelegate().removeImage(storedImage.getKey()); } } } @@ -216,7 +218,7 @@ public class DefaultSynchronisationState implements SynchronisationState { try { - return context.getRepositoryDelegate().saveRepository(new Repository(repositoryName)); + return context.getRepositoryDelegate().saveRepository(new Repository(new RepositoryKey(repositoryName))); } catch (SaveException e) { LOGGER.error("Tried to save new repository during sync but failed", e); @@ -230,15 +232,16 @@ public class DefaultSynchronisationState implements SynchronisationState { * 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, DockerImage dockerHubImage, SynchronisationContext context) { + private Image configureImage(Repository repository, DockerImage dockerHubImage, SynchronisationContext context) { - Image image = context.getImageDelegate().findImageByRepositoryAndImageName(repositoryId, dockerHubImage.getName()); + final ImageKey baseImageKey = new ImageKey(dockerHubImage.getName(), repository.getKey()); + final Image image = context.getImageDelegate().findImageByRepositoryAndImageName(baseImageKey); if (isImageNew(image)) { try { - return context.getImageDelegate().saveImage(new Image(repositoryId, dockerHubImage.getName())); + return context.getImageDelegate().saveImage(Image.makeFromKey(baseImageKey)); } catch (SaveException e) { LOGGER.error("Tried to save new image during sync but failed", e); diff --git a/src/main/java/io/linuxserver/fleet/web/pages/HomePage.java b/src/main/java/io/linuxserver/fleet/web/pages/HomePage.java index 4845ea9..711f589 100644 --- a/src/main/java/io/linuxserver/fleet/web/pages/HomePage.java +++ b/src/main/java/io/linuxserver/fleet/web/pages/HomePage.java @@ -51,7 +51,7 @@ public class HomePage extends WebPage { for (Repository repository : repositories) { if (repository.isSyncEnabled()) - populatedRepositories.add(new RepositoryWithImages(repository, imageDelegate.fetchImagesByRepository(repository.getId()))); + populatedRepositories.add(new RepositoryWithImages(repository, imageDelegate.fetchImagesByRepository(repository.getKey()))); } model.put("populatedRepositories", populatedRepositories); diff --git a/src/main/java/io/linuxserver/fleet/web/routes/AllImagesApi.java b/src/main/java/io/linuxserver/fleet/web/routes/AllImagesApi.java index e2345e8..5e5e4b3 100644 --- a/src/main/java/io/linuxserver/fleet/web/routes/AllImagesApi.java +++ b/src/main/java/io/linuxserver/fleet/web/routes/AllImagesApi.java @@ -55,7 +55,7 @@ public class AllImagesApi implements Route { if (repository.isSyncEnabled()) { - List savedImages = imageDelegate.fetchImagesByRepository(repository.getId()); + List savedImages = imageDelegate.fetchImagesByRepository(repository.getKey()); List apiImages = new ArrayList<>(); for (Image savedImage : savedImages) { diff --git a/src/main/java/io/linuxserver/fleet/web/routes/GetImageApi.java b/src/main/java/io/linuxserver/fleet/web/routes/GetImageApi.java index 20994a2..9778e8d 100644 --- a/src/main/java/io/linuxserver/fleet/web/routes/GetImageApi.java +++ b/src/main/java/io/linuxserver/fleet/web/routes/GetImageApi.java @@ -18,9 +18,11 @@ package io.linuxserver.fleet.web.routes; import io.linuxserver.fleet.delegate.ImageDelegate; +import io.linuxserver.fleet.model.api.ApiImage; import io.linuxserver.fleet.model.internal.Image; import io.linuxserver.fleet.model.api.ApiResponse; import io.linuxserver.fleet.model.api.FleetApiException; +import io.linuxserver.fleet.model.key.ImageKey; import spark.Request; import spark.Response; import spark.Route; @@ -36,16 +38,16 @@ public class GetImageApi implements Route { @Override public Object handle(Request request, Response response) { - String imageIdParam = request.queryParams("imageId"); - if (null == imageIdParam) { - throw new FleetApiException(400, "Missing imageId param"); + String imageKeyParam = request.queryParams("imageKey"); + if (null == imageKeyParam) { + throw new FleetApiException(400, "Missing imageKey param"); } - Image image = imageDelegate.fetchImage(Integer.parseInt(imageIdParam)); + Image image = imageDelegate.fetchImage(ImageKey.parse(imageKeyParam)); if (null == image) { throw new FleetApiException(404, "Image not found"); } - return new ApiResponse<>("OK", image); + return new ApiResponse<>("OK", ApiImage.fromImage(image)); } } diff --git a/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java b/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java index 708a8e7..4ab37cf 100644 --- a/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java +++ b/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java @@ -23,6 +23,7 @@ import io.linuxserver.fleet.model.internal.ImagePullStat; import io.linuxserver.fleet.model.api.ApiImagePullHistory; import io.linuxserver.fleet.model.api.ApiResponse; import io.linuxserver.fleet.model.api.FleetApiException; +import io.linuxserver.fleet.model.key.ImageKey; import spark.Request; import spark.Response; import spark.Route; @@ -40,15 +41,15 @@ public class GetImagePullHistoryApi implements Route { @Override public Object handle(Request request, Response response) { - String imageIdParam = request.queryParams("imageId"); - if (null == imageIdParam) { + String imageKeyParam = request.queryParams("imageKey"); + if (null == imageKeyParam) { throw new FleetApiException(400, "Missing imageId param"); } - int imageId = Integer.parseInt(imageIdParam); - List imagePullStats = imageDelegate.fetchImagePullHistory(imageId, getGroupMode(request)); + final ImageKey lookupKey = ImageKey.parse(imageKeyParam); + List imagePullStats = imageDelegate.fetchImagePullHistory(lookupKey, getGroupMode(request)); - Image image = imageDelegate.fetchImage(imageId); + Image image = imageDelegate.fetchImage(lookupKey); return new ApiResponse<>("OK", ApiImagePullHistory.fromPullStats(image, imagePullStats)); } diff --git a/src/main/java/io/linuxserver/fleet/web/routes/ManageImageApi.java b/src/main/java/io/linuxserver/fleet/web/routes/ManageImageApi.java index 00c4b7a..755470d 100644 --- a/src/main/java/io/linuxserver/fleet/web/routes/ManageImageApi.java +++ b/src/main/java/io/linuxserver/fleet/web/routes/ManageImageApi.java @@ -21,6 +21,7 @@ import io.linuxserver.fleet.delegate.ImageDelegate; import io.linuxserver.fleet.model.internal.Image; import io.linuxserver.fleet.model.api.ApiResponse; import io.linuxserver.fleet.model.api.FleetApiException; +import io.linuxserver.fleet.model.key.ImageKey; import spark.Request; import spark.Response; import spark.Route; @@ -38,9 +39,9 @@ public class ManageImageApi implements Route { try { - int imageId = Integer.parseInt(request.queryParams("imageId")); + String imageKeyParam = request.queryParams("imageKey"); - Image image = imageDelegate.fetchImage(imageId); + Image image = imageDelegate.fetchImage(ImageKey.parse(imageKeyParam)); if (null == image) { response.status(404); diff --git a/src/main/java/io/linuxserver/fleet/web/routes/ManageRepositoryApi.java b/src/main/java/io/linuxserver/fleet/web/routes/ManageRepositoryApi.java index 682c4b3..c3762fa 100644 --- a/src/main/java/io/linuxserver/fleet/web/routes/ManageRepositoryApi.java +++ b/src/main/java/io/linuxserver/fleet/web/routes/ManageRepositoryApi.java @@ -21,6 +21,7 @@ import io.linuxserver.fleet.delegate.RepositoryDelegate; import io.linuxserver.fleet.model.internal.Repository; import io.linuxserver.fleet.model.api.ApiResponse; import io.linuxserver.fleet.model.api.FleetApiException; +import io.linuxserver.fleet.model.key.RepositoryKey; import spark.Request; import spark.Response; import spark.Route; @@ -40,7 +41,7 @@ public class ManageRepositoryApi implements Route { int repositoryId = Integer.parseInt(request.queryParams("repositoryId")); - Repository repository = repositoryDelegate.fetchRepository(repositoryId); + Repository repository = repositoryDelegate.fetchRepository(new RepositoryKey(repositoryId)); if (null == repository) { response.status(404); diff --git a/src/main/resources/assets/css/fleet.css b/src/main/resources/assets/css/fleet.css index f7cc925..d933181 100644 --- a/src/main/resources/assets/css/fleet.css +++ b/src/main/resources/assets/css/fleet.css @@ -78,6 +78,7 @@ input[type="text"], textarea { .table tbody tr td { border-color: #eaeaea; + white-space: nowrap; } .table tbody tr td a { diff --git a/src/main/resources/assets/js/fleet.js b/src/main/resources/assets/js/fleet.js index 0fe2882..b04a94b 100644 --- a/src/main/resources/assets/js/fleet.js +++ b/src/main/resources/assets/js/fleet.js @@ -111,10 +111,10 @@ var imageListManager = (function($) { var showImage = function() { - var option = $(this); - var row = getImageRow(option); - var imageId = getImageId(row); - var request = buildRequest('SHOW', imageId) + var option = $(this); + var row = getImageRow(option); + var imageKey = getImageKey(row); + var request = buildRequest('SHOW', imageKey) ajaxManager.call(request, function(data) { @@ -125,10 +125,10 @@ var imageListManager = (function($) { var hideImage = function() { - var option = $(this); - var row = getImageRow(option); - var imageId = getImageId(row); - var request = buildRequest('HIDE', imageId) + var option = $(this); + var row = getImageRow(option); + var imageKey = getImageKey(row); + var request = buildRequest('HIDE', imageKey) var call = ajaxManager.call(request, function(data) { @@ -139,10 +139,10 @@ var imageListManager = (function($) { var markImageUnstable = function() { - var option = $(this); - var row = getImageRow(option); - var imageId = getImageId(row); - var request = buildRequest('UNSTABLE', imageId) + var option = $(this); + var row = getImageRow(option); + var imageKey = getImageKey(row); + var request = buildRequest('UNSTABLE', imageKey) var call = ajaxManager.call(request, function(data) { @@ -154,10 +154,10 @@ var imageListManager = (function($) { var markImageStable = function() { - var option = $(this); - var row = getImageRow(option); - var imageId = getImageId(row); - var request = buildRequest('STABLE', imageId) + var option = $(this); + var row = getImageRow(option); + var imageKey = getImageKey(row); + var request = buildRequest('STABLE', imageKey) var call = ajaxManager.call(request, function(data) { @@ -168,31 +168,31 @@ var imageListManager = (function($) { var removeDeprecationNotice = function() { - var option = $(this); - var row = getImageRow(option); - var imageId = getImageId(row); - var request = buildRequest('RESTORE', imageId) + var option = $(this); + var row = getImageRow(option); + var imageKey = getImageKey(row); + var request = buildRequest('RESTORE', imageKey) var call = ajaxManager.call(request, function(data) { row.removeClass('deprecated-image'); row.find('.deprecation-message').remove(); - option.replaceWith(createDeprecationButton(imageId)); + option.replaceWith(createDeprecationButton(imageKey.split(':')[1])); }); }; var showImageVersionMask = function(event) { - var modal = $(this); - var option = $(event.relatedTarget); - var row = getImageRow(option); - var imageId = getImageId(row); + var modal = $(this); + var option = $(event.relatedTarget); + var row = getImageRow(option); + var imageKey = getImageKey(row); modal.find('#selected-mask-image-name').text('Version mask for ' + getImageName(row)); - modal.find('#submit-version-mask-change').data('image-id', imageId); + modal.find('#submit-version-mask-change').data('image-key', imageKey); - getImageMask(imageId, function(image) { + getImageMask(imageKey, function(image) { modal.find('#image-version-mask').val(image.data.versionMask); }); }; @@ -200,7 +200,7 @@ var imageListManager = (function($) { var submitVersionMaskChange = function() { var button = $(this); - var request = buildRequest('MASK', button.data('image-id')); + var request = buildRequest('MASK', button.data('image-key')); request.data.versionMask = $('#image-version-mask').val(); ajaxManager.call(request, function() { @@ -210,22 +210,22 @@ var imageListManager = (function($) { var showImageDeprecationNotice = function(event) { - var modal = $(this); - var option = $(event.relatedTarget); - var row = getImageRow(option); - var imageId = getImageId(row); + var modal = $(this); + var option = $(event.relatedTarget); + var row = getImageRow(option); + var imageKey = getImageKey(row); modal.find('#image-deprecation-reason').val(''); modal.find('#selected-deprecation-image-name').text('Deprecation notice for ' + getImageName(row)); - modal.find('#submit-deprecation-change').data('image-id', imageId); + modal.find('#submit-deprecation-change').data('image-key', imageKey); modal.find('#submit-deprecation-change').data('trigger-option', option.attr('id')); }; var submitDeprecationNotice = function() { - var button = $(this); - var imageId = button.data('image-id'); - var request = buildRequest('DEPRECATE', imageId); + var button = $(this); + var imageKey = button.data('image-key'); + var request = buildRequest('DEPRECATE', imageKey); request.data.deprecationReason = $('#image-deprecation-reason').val(); @@ -233,7 +233,6 @@ var imageListManager = (function($) { var trigger = $('#' + button.data('trigger-option')); var row = getImageRow(trigger); - var imageId = getImageId(row); row.addClass('deprecated-image'); row.find('.image-name').append(createRowDeprecationMessage(data.data.deprecationReason)); @@ -244,14 +243,14 @@ var imageListManager = (function($) { }); }; - var buildRequest = function(action, imageId) { + var buildRequest = function(action, imageKey) { return { url: '/admin/api/manageImage', method: 'POST', data: { action: action, - imageId: imageId + imageKey: imageKey } }; }; @@ -272,8 +271,8 @@ var imageListManager = (function($) { return $(''); }; - var createDeprecationButton = function(imageId) { - return $(''); + var createDeprecationButton = function(imageKey) { + return $(''); }; var createRestoreButton = function() { @@ -296,18 +295,18 @@ var imageListManager = (function($) { return item.parents('tr'); }; - var getImageId = function(row) { - return parseInt(row.data('image-id')); + var getImageKey = function(row) { + return row.data('image-key'); }; var getImageName = function(row) { return row.data('image-name'); }; - var getImageMask = function(imageId, callback) { + var getImageMask = function(imageKey, callback) { var request = { - url: '/admin/api/getImage?imageId=' + imageId, + url: '/admin/api/getImage?imageKey=' + imageKey, method: 'GET' }; @@ -340,14 +339,14 @@ var imageSearchManager = (function($) { var performSearch = function() { var $searchBox = $(this); - var currentSearch = $.trim($searchBox.val()); + var currentSearch = $.trim($searchBox.val()).toLowerCase(); var activeTable = $('#' + $searchBox.data('repository-id') + '_images'); var rows = activeTable.find('tbody tr'); rows.each(function(i, row) { var $row = $(row); - var imageName = $row.data('image-name'); + var imageName = $row.data('image-name').toLowerCase(); if (imageName.startsWith(currentSearch) || currentSearch.length === 0) { $row.show(); diff --git a/src/main/resources/db/migration/V1.10__UpdateImageViewWithRepository.sql b/src/main/resources/db/migration/V1.10__UpdateImageViewWithRepository.sql new file mode 100644 index 0000000..4969880 --- /dev/null +++ b/src/main/resources/db/migration/V1.10__UpdateImageViewWithRepository.sql @@ -0,0 +1,24 @@ +DELIMITER // + +CREATE OR REPLACE VIEW `Image_View` AS ( + + SELECT + images.`id` AS `ImageId`, + images.`repository` AS `RepositoryId`, + repositories.`name` AS `RepositoryName`, + images.`name` AS `ImageName`, + images.`pulls` AS `ImagePullCount`, + images.`latest_version` AS `LatestTagVersion`, + images.`latest_version_raw` AS `LatestMaskedTagVersion`, + images.`latest_version_buildtime` AS `LatestTagBuildDate`, + images.`version_mask` AS `ImageVersionMask`, + images.`hidden` AS `ImageHidden`, + images.`unstable` AS `ImageUnstable`, + images.`deprecated` AS `ImageDeprecated`, + images.`deprecation_reason` AS `ImageDeprecationReason`, + images.`modified` AS `ModifiedTime` + FROM + Images images + JOIN Repositories repositories ON repositories.`id` = images.`repository` +); +// \ No newline at end of file diff --git a/src/main/resources/spark/template/freemarker/admin.ftl b/src/main/resources/spark/template/freemarker/admin.ftl index 8e1ea10..65ca1db 100644 --- a/src/main/resources/spark/template/freemarker/admin.ftl +++ b/src/main/resources/spark/template/freemarker/admin.ftl @@ -34,7 +34,7 @@ <#list repositories as repository> - + ${repository.name} diff --git a/src/main/resources/spark/template/freemarker/home.ftl b/src/main/resources/spark/template/freemarker/home.ftl index 855f0cd..c40d551 100644 --- a/src/main/resources/spark/template/freemarker/home.ftl +++ b/src/main/resources/spark/template/freemarker/home.ftl @@ -10,7 +10,7 @@ @@ -21,7 +21,7 @@
<#list populatedRepositories as populatedRepository> -
+
<#if populatedRepository.everyImageStable> @@ -48,15 +48,15 @@
- Search ${populatedRepository.repository.name} + Search ${populatedRepository.repository.name}
- +
-
+
@@ -73,7 +73,7 @@ <#list populatedRepository.images as image> <#if !image.hidden || __AUTHENTICATED_USER?has_content> - +
${populatedRepository.repository.name} / ${image.name} <#if image.deprecated> @@ -105,10 +105,10 @@ <#if __AUTHENTICATED_USER?has_content>