From 594125bed660a46c1e9dc33c9c4e51a60b8552f1 Mon Sep 17 00:00:00 2001 From: Josh Stark Date: Thu, 25 Apr 2019 12:12:11 +0100 Subject: [PATCH 1/6] Added pull history API. Included removal process during sync for deleted images --- build.gradle | 2 +- .../io/linuxserver/fleet/core/FleetApp.java | 3 +- .../fleet/db/dao/DefaultImageDAO.java | 75 ++++++- .../fleet/db/dao/DefaultRepositoryDAO.java | 32 ++- .../io/linuxserver/fleet/db/dao/ImageDAO.java | 5 + .../io/linuxserver/fleet/db/dao/Utils.java | 12 + .../fleet/delegate/ImageDelegate.java | 9 + .../fleet/model/ImagePullStat.java | 60 +++++ .../fleet/model/api/ApiImagePullHistory.java | 80 +++++++ .../sync/DefaultSynchronisationState.java | 34 ++- .../web/routes/GetImagePullHistoryApi.java | 55 +++++ .../V1.8__PullHistoryAndImageMeta.sql | 207 ++++++++++++++++++ 12 files changed, 558 insertions(+), 16 deletions(-) create mode 100644 src/main/java/io/linuxserver/fleet/model/ImagePullStat.java create mode 100644 src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java create mode 100644 src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java create mode 100644 src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql diff --git a/build.gradle b/build.gradle index 1e24572..c78c784 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ repositories { mavenCentral() } -version = '1.2.1' +version = '1.3.0' sourceSets { diff --git a/src/main/java/io/linuxserver/fleet/core/FleetApp.java b/src/main/java/io/linuxserver/fleet/core/FleetApp.java index ee77a3c..b35749b 100644 --- a/src/main/java/io/linuxserver/fleet/core/FleetApp.java +++ b/src/main/java/io/linuxserver/fleet/core/FleetApp.java @@ -130,7 +130,8 @@ public class FleetApp { */ path("/api/v1", () -> { - get("/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()), new JsonTransformer()); + get("/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()), new JsonTransformer()); + get("/pullHistory", new GetImagePullHistoryApi(beans.getImageDelegate()), new JsonTransformer()); after("/*", (request, response) -> { 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 fbbd245..46e063e 100644 --- a/src/main/java/io/linuxserver/fleet/db/dao/DefaultImageDAO.java +++ b/src/main/java/io/linuxserver/fleet/db/dao/DefaultImageDAO.java @@ -22,6 +22,7 @@ import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.db.query.InsertUpdateStatus; import io.linuxserver.fleet.db.query.LimitedResult; import io.linuxserver.fleet.model.Image; +import io.linuxserver.fleet.model.ImagePullStat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,9 +30,7 @@ import java.sql.*; import java.util.ArrayList; import java.util.List; -import static io.linuxserver.fleet.db.dao.Utils.setNullableInt; -import static io.linuxserver.fleet.db.dao.Utils.setNullableLong; -import static io.linuxserver.fleet.db.dao.Utils.setNullableString; +import static io.linuxserver.fleet.db.dao.Utils.*; public class DefaultImageDAO implements ImageDAO { @@ -46,9 +45,11 @@ public class DefaultImageDAO implements ImageDAO { @Override public Image findImageByRepositoryAndImageName(int repositoryId, String imageName) { + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - CallableStatement call = connection.prepareCall("{CALL Image_GetByName(?,?)}"); + call = connection.prepareCall("{CALL Image_GetByName(?,?)}"); call.setInt(1, repositoryId); call.setString(2, imageName); @@ -58,6 +59,8 @@ public class DefaultImageDAO implements ImageDAO { } catch (SQLException e) { LOGGER.error("Unable to fetch image", e); + } finally { + safeClose(call); } return null; @@ -68,9 +71,11 @@ public class DefaultImageDAO implements ImageDAO { LOGGER.debug("Fetching image by ID: " + id); + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - CallableStatement call = connection.prepareCall("{CALL Image_Get(?)}"); + call = connection.prepareCall("{CALL Image_Get(?)}"); call.setInt(1, id); ResultSet results = call.executeQuery(); @@ -79,6 +84,8 @@ public class DefaultImageDAO implements ImageDAO { } catch (SQLException e) { LOGGER.error("Unable to fetch image.", e); + } finally { + safeClose(call); } return null; @@ -89,9 +96,11 @@ public class DefaultImageDAO implements ImageDAO { List images = new ArrayList<>(); + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - CallableStatement call = connection.prepareCall("{CALL Image_GetAll(?,?)}"); + call = connection.prepareCall("{CALL Image_GetAll(?,?)}"); call.setInt(1, repositoryId); call.registerOutParameter(2, Types.INTEGER); @@ -104,6 +113,8 @@ public class DefaultImageDAO implements ImageDAO { } catch (SQLException e) { LOGGER.error("Unable to get all images", e); + } finally { + safeClose(call); } return new LimitedResult<>(images, images.size()); @@ -112,9 +123,11 @@ public class DefaultImageDAO implements ImageDAO { @Override public InsertUpdateResult saveImage(Image image) { + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - CallableStatement call = connection.prepareCall("{CALL Image_Save(?,?,?,?,?,?,?,?,?,?,?,?,?)"); + call = connection.prepareCall("{CALL Image_Save(?,?,?,?,?,?,?,?,?,?,?,?,?)"); setNullableInt(call, 1, image.getId()); call.setInt(2, image.getRepositoryId()); call.setString(3, image.getName()); @@ -145,24 +158,70 @@ public class DefaultImageDAO implements ImageDAO { LOGGER.error("Unable to save image", e); return new InsertUpdateResult<>(null, InsertUpdateStatus.OK, "Unable to save image"); + + } finally { + safeClose(call); } } @Override public void removeImage(Integer id) { + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - PreparedStatement call = connection.prepareStatement("DELETE FROM Images WHERE `id` = ?"); + call = connection.prepareCall("{CALL Image_Delete(?)}"); call.setInt(1, id); call.executeUpdate(); + call.close(); } catch (SQLException e) { LOGGER.error("Error when removing image", e); + } finally { + safeClose(call); } } + @Override + public List fetchImagePullHistory(Integer imageId, ImagePullStat.GroupMode groupMode) { + + List pullHistory = new ArrayList<>(); + + CallableStatement call = null; + + try (Connection connection = databaseConnection.getConnection()) { + + call = connection.prepareCall("CALL Image_GetPullHistory(?, ?)"); + call.setInt(1, imageId); + call.setString(2, groupMode.toString()); + + ResultSet results = call.executeQuery(); + while (results.next()) + pullHistory.add(parseImagePullHistoryFromResultSet(results, groupMode)); + + call.close(); + + } catch (SQLException e) { + LOGGER.error("Error when fetching image pull history", e); + } finally { + safeClose(call); + } + + return pullHistory; + } + + private ImagePullStat parseImagePullHistoryFromResultSet(ResultSet results, ImagePullStat.GroupMode groupMode) throws SQLException { + + return new ImagePullStat( + results.getInt("ImageId"), + results.getString("TimeGroup"), + results.getLong("ImagePulls"), + groupMode + ); + } + private Image parseImageFromResultSet(ResultSet results) throws SQLException { Image image = new Image( 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 68a6ff6..2672ff9 100644 --- a/src/main/java/io/linuxserver/fleet/db/dao/DefaultRepositoryDAO.java +++ b/src/main/java/io/linuxserver/fleet/db/dao/DefaultRepositoryDAO.java @@ -28,6 +28,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; @@ -44,9 +45,11 @@ public class DefaultRepositoryDAO implements RepositoryDAO { @Override public Repository fetchRepository(int id) { + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - CallableStatement call = connection.prepareCall("{CALL Repository_Get(?)}"); + call = connection.prepareCall("{CALL Repository_Get(?)}"); call.setInt(1, id); ResultSet results = call.executeQuery(); @@ -55,6 +58,8 @@ public class DefaultRepositoryDAO implements RepositoryDAO { } catch (SQLException e) { LOGGER.error("Unable to retrieve repository", e); + } finally { + safeClose(call); } return null; @@ -63,9 +68,11 @@ public class DefaultRepositoryDAO implements RepositoryDAO { @Override public InsertUpdateResult saveRepository(Repository repository) { + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - CallableStatement call = connection.prepareCall("{CALL Repository_Save(?,?,?,?,?,?,?)}"); + call = connection.prepareCall("{CALL Repository_Save(?,?,?,?,?,?,?)}"); setNullableInt(call, 1, repository.getId()); call.setString(2, repository.getName()); setNullableString(call, 3, repository.getVersionMask()); @@ -90,6 +97,9 @@ public class DefaultRepositoryDAO implements RepositoryDAO { LOGGER.error("Unable to save repository", e); return new InsertUpdateResult<>(null, InsertUpdateStatus.OK, "Unable to save repository"); + + } finally { + safeClose(call); } } @@ -98,9 +108,11 @@ public class DefaultRepositoryDAO implements RepositoryDAO { List repositories = new ArrayList<>(); + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - CallableStatement call = connection.prepareCall("{CALL Repository_GetAll()}"); + call = connection.prepareCall("{CALL Repository_GetAll()}"); ResultSet results = call.executeQuery(); while (results.next()) @@ -108,6 +120,8 @@ public class DefaultRepositoryDAO implements RepositoryDAO { } catch (SQLException e) { LOGGER.error("Unable to get all repositories", e); + } finally { + safeClose(call); } return repositories; @@ -116,9 +130,11 @@ public class DefaultRepositoryDAO implements RepositoryDAO { @Override public Repository findRepositoryByName(String name) { + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - CallableStatement call = connection.prepareCall("{CALL Repository_GetByName(?)}"); + call = connection.prepareCall("{CALL Repository_GetByName(?)}"); call.setString(1, name); ResultSet results = call.executeQuery(); @@ -127,6 +143,8 @@ public class DefaultRepositoryDAO implements RepositoryDAO { } catch (SQLException e) { LOGGER.error("Unable to retrieve repository", e); + } finally { + safeClose(call); } return null; @@ -135,15 +153,19 @@ public class DefaultRepositoryDAO implements RepositoryDAO { @Override public void removeRepository(int id) { + CallableStatement call = null; + try (Connection connection = databaseConnection.getConnection()) { - CallableStatement call = connection.prepareCall("{CALL Repository_Delete(?)}"); + call = connection.prepareCall("{CALL Repository_Delete(?)}"); call.setInt(1, id); call.executeUpdate(); } catch (SQLException e) { LOGGER.error("Error when removing repository", e); + } finally { + safeClose(call); } } 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 3851f66..9840438 100644 --- a/src/main/java/io/linuxserver/fleet/db/dao/ImageDAO.java +++ b/src/main/java/io/linuxserver/fleet/db/dao/ImageDAO.java @@ -3,6 +3,9 @@ package io.linuxserver.fleet.db.dao; import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.db.query.LimitedResult; import io.linuxserver.fleet.model.Image; +import io.linuxserver.fleet.model.ImagePullStat; + +import java.util.List; public interface ImageDAO { @@ -15,4 +18,6 @@ public interface ImageDAO { InsertUpdateResult saveImage(Image image); void removeImage(Integer id); + + List fetchImagePullHistory(Integer imageId, ImagePullStat.GroupMode groupMode); } diff --git a/src/main/java/io/linuxserver/fleet/db/dao/Utils.java b/src/main/java/io/linuxserver/fleet/db/dao/Utils.java index 7b5a65b..2e7fab0 100644 --- a/src/main/java/io/linuxserver/fleet/db/dao/Utils.java +++ b/src/main/java/io/linuxserver/fleet/db/dao/Utils.java @@ -49,4 +49,16 @@ class Utils { else call.setString(position, value); } + + static void safeClose(CallableStatement call) { + + try { + + if (null != call) + call.close(); + + } catch (SQLException e) { + throw new RuntimeException("Unable to close call", e); + } + } } diff --git a/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java b/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java index 9b57ed8..d6e373d 100644 --- a/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java +++ b/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.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.Image; +import io.linuxserver.fleet.model.ImagePullStat; import java.util.List; @@ -65,4 +66,12 @@ public class ImageDelegate { throw new SaveException(result.getStatusMessage()); } + + public List fetchImagePullHistory(int id) { + return fetchImagePullHistory(id, ImagePullStat.GroupMode.DAY); + } + + public List fetchImagePullHistory(int id, ImagePullStat.GroupMode groupMode) { + return imageDAO.fetchImagePullHistory(id, groupMode); + } } diff --git a/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java b/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java new file mode 100644 index 0000000..a5f1d34 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 LinuxServer.io + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.linuxserver.fleet.model; + +public class ImagePullStat { + + private final int imageId; + private final String timeGroup; + private final long pullCount; + private final GroupMode groupMode; + + public ImagePullStat(int imageId, String timeGroup, long pullCount, GroupMode groupMode) { + + this.imageId = imageId; + this.timeGroup = timeGroup; + this.pullCount = pullCount; + this.groupMode = groupMode; + } + + public int getImageId() { + return imageId; + } + + public String getTimeGroup() { + return timeGroup; + } + + public long getPullCount() { + return pullCount; + } + + public GroupMode getGroupMode() { + return groupMode; + } + + public enum GroupMode { + + DAY, WEEK, MONTH, YEAR; + + @Override + public String toString() { + return name().toLowerCase(); + } + } +} diff --git a/src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java b/src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java new file mode 100644 index 0000000..856c1fc --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java @@ -0,0 +1,80 @@ +/* + * 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.api; + +import io.linuxserver.fleet.model.Image; +import io.linuxserver.fleet.model.ImagePullStat; + +import java.util.ArrayList; +import java.util.List; + +public class ApiImagePullHistory { + + private int imageId; + private String imageName; + private String groupMode; + private List pullHistory = new ArrayList<>(); + + public int getImageId() { + return imageId; + } + + public String getImageName() { + return imageName; + } + + public String getGroupMode() { + return groupMode; + } + + public List getPullHistory() { + return pullHistory; + } + + public static ApiImagePullHistory fromPullStats(Image image, List stats) { + + ApiImagePullHistory history = new ApiImagePullHistory(); + history.imageId = image.getId(); + history.imageName = image.getName(); + history.groupMode = stats.get(0).getGroupMode().toString(); + + for (ImagePullStat stat : stats) + history.pullHistory.add(new ApiImagePullStat(stat.getTimeGroup(), stat.getPullCount())); + + return history; + } + + public static class ApiImagePullStat { + + private final String timeGroup; + private final long pullCount; + + ApiImagePullStat(String timeGroup, long pullCount) { + this.timeGroup = timeGroup; + this.pullCount = pullCount; + } + + public String getTimeGroup() { + return timeGroup; + } + + public long getPullCount() { + return pullCount; + } + } +} diff --git a/src/main/java/io/linuxserver/fleet/sync/DefaultSynchronisationState.java b/src/main/java/io/linuxserver/fleet/sync/DefaultSynchronisationState.java index e31d7cd..0650814 100644 --- a/src/main/java/io/linuxserver/fleet/sync/DefaultSynchronisationState.java +++ b/src/main/java/io/linuxserver/fleet/sync/DefaultSynchronisationState.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; public class DefaultSynchronisationState implements SynchronisationState { @@ -86,7 +87,9 @@ public class DefaultSynchronisationState implements SynchronisationState { try { List repositories = context.getDockerHubDelegate().fetchAllRepositories(); + onRepositoryScanned(context, repositories); + checkAndRemoveMissingRepositories(repositories, context); for (String repositoryName : repositories) synchroniseRepository(repositoryName, context); @@ -122,6 +125,7 @@ public class DefaultSynchronisationState implements SynchronisationState { if (repository.isSyncEnabled()) { List images = context.getDockerHubDelegate().fetchAllImagesFromRepository(repository.getName()); + checkAndRemoveMissingImages(repository, images, context); int totalSize = images.size(); LOGGER.info("Found {} images in Docker Hub", totalSize); @@ -139,7 +143,7 @@ public class DefaultSynchronisationState implements SynchronisationState { image.withPullCount(dockerHubImage.getPullCount()).withVersion(maskedVersion); context.getImageDelegate().saveImage(image); - onImageUpdated(context, new ImageUpdateEvent(image, i, totalSize)); + onImageUpdated(context, new ImageUpdateEvent(image, i + 1, totalSize)); } catch (SaveException e) { LOGGER.error("Unable to save updated image", e); @@ -151,6 +155,34 @@ public class DefaultSynchronisationState implements SynchronisationState { } } + private void checkAndRemoveMissingRepositories(List repositories, SynchronisationContext context) { + + LOGGER.info("Checking for any removed repositories."); + for (Repository storedRepository : context.getRepositoryDelegate().fetchAllRepositories()) { + + if (!repositories.contains(storedRepository.getName())) { + + LOGGER.info("Found repository which no longer exists in Docker Hub. Removing {}", storedRepository.getName()); + context.getRepositoryDelegate().removeRepository(storedRepository.getId()); + } + } + } + + private void checkAndRemoveMissingImages(Repository repository, List images, SynchronisationContext context) { + + List dockerHubImageNames = images.stream().map(DockerHubImage::getName).collect(Collectors.toList()); + + LOGGER.info("Checking for any removed images under {}", repository.getName()); + for (Image storedImage : context.getImageDelegate().fetchImagesByRepository(repository.getId())) { + + if (!dockerHubImageNames.contains(storedImage.getName())) { + + LOGGER.info("Found image which no longer exists in Docker Hub. Removing {}", storedImage.getName()); + context.getImageDelegate().removeImage(storedImage.getId()); + } + } + } + private String getVersionMask(String repositoryMask, String imageMask) { return imageMask == null ? repositoryMask : imageMask; } diff --git a/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java b/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java new file mode 100644 index 0000000..c6f4725 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.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.web.routes; + +import io.linuxserver.fleet.delegate.ImageDelegate; +import io.linuxserver.fleet.model.Image; +import io.linuxserver.fleet.model.ImagePullStat; +import io.linuxserver.fleet.model.api.ApiImagePullHistory; +import io.linuxserver.fleet.model.api.ApiResponse; +import io.linuxserver.fleet.model.api.FleetApiException; +import spark.Request; +import spark.Response; +import spark.Route; + +import java.util.List; + +public class GetImagePullHistoryApi implements Route { + + private final ImageDelegate imageDelegate; + + public GetImagePullHistoryApi(ImageDelegate imageDelegate) { + this.imageDelegate = imageDelegate; + } + + @Override + public Object handle(Request request, Response response) { + + String imageIdParam = request.queryParams("imageId"); + if (null == imageIdParam) { + throw new FleetApiException(400, "Missing imageId param"); + } + + int imageId = Integer.parseInt(imageIdParam); + + Image image = imageDelegate.fetchImage(imageId); + List imagePullStats = imageDelegate.fetchImagePullHistory(imageId); + + return new ApiResponse<>("OK", ApiImagePullHistory.fromPullStats(image, imagePullStats)); + } +} diff --git a/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql b/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql new file mode 100644 index 0000000..a5e47c6 --- /dev/null +++ b/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql @@ -0,0 +1,207 @@ +DELIMITER // + +CREATE TABLE ImagePullHistory ( + `image_id` INT NOT NULL, + `pull_timestamp` BIGINT NOT NULL, + `pull_count` BIGINT NOT NULL, + PRIMARY KEY (`image_id`, `pull_timestamp`) +) ENGINE=InnoDB; +// + +CREATE PROCEDURE `Image_SavePullHistory` +( + in_image_id INT, + in_image_pulls BIGINT, + + OUT out_status INT, + OUT out_message VARCHAR(100) +) +BEGIN + + IF EXISTS(SELECT 1 FROM Images WHERE `id` = in_image_id) THEN + + INSERT INTO ImagePullHistory + ( + `image_id`, + `pull_timestamp`, + `pull_count` + ) + VALUES + ( + in_image_id, + UNIX_TIMESTAMP(NOW()), + in_image_pulls + ); + + END IF; + + SET out_status = 0; + SET out_message = "OK"; + +END; +// + +CREATE PROCEDURE `Image_GetPullHistory` +( + in_image_id INT, + in_grouping_mode ENUM('day', 'week', 'month', 'year') +) +BEGIN + + IF in_grouping_mode = 'day' THEN + + SELECT + `image_id` AS ImageId, + MAX(`pull_count`) AS ImagePulls, + FROM_UNIXTIME(`pull_timestamp`, '%d%m%Y') AS TimeGroup + FROM + ImagePullHistory + WHERE + `image_id` = in_image_id + GROUP BY `image_id`, TimeGroup + ORDER BY TimeGroup; + + ELSEIF in_grouping_mode = 'week' THEN + + SELECT + `image_id` AS ImageId, + MAX(`pull_count`) AS ImagePulls, + FROM_UNIXTIME(`pull_timestamp`, '%v%Y') AS TimeGroup + FROM + ImagePullHistory + WHERE + `image_id` = in_image_id + GROUP BY `image_id`, TimeGroup + ORDER BY TimeGroup; + + ELSEIF in_grouping_mode = 'month' THEN + + SELECT + `image_id` AS ImageId, + MAX(`pull_count`) AS ImagePulls, + FROM_UNIXTIME(`pull_timestamp`, '%m%Y') AS TimeGroup + FROM + ImagePullHistory + WHERE + `image_id` = in_image_id + GROUP BY `image_id`, TimeGroup + ORDER BY TimeGroup; + + ELSEIF in_grouping_mode = 'year' THEN + + SELECT + `image_id` AS ImageId, + MAX(`pull_count`) AS ImagePulls, + FROM_UNIXTIME(`pull_timestamp`, '%Y') AS TimeGroup + FROM + ImagePullHistory + WHERE + `image_id` = in_image_id + GROUP BY `image_id`, TimeGroup + ORDER BY TimeGroup; + + END IF; + +END; +// + +DROP PROCEDURE `Image_Save`// +CREATE PROCEDURE `Image_Save` +( + in_id INT, + in_repository INT, + in_name VARCHAR(255), + in_pull_count BIGINT, + in_version VARCHAR(100), + in_version_mask VARCHAR(255), + in_hidden TINYINT, + in_unstable TINYINT, + in_deprecated TINYINT, + in_deprecation_reason VARCHAR(255), + + OUT out_id INT, + OUT out_status INT, + OUT out_message VARCHAR(100) +) +BEGIN + + IF in_id IS NULL THEN + + INSERT INTO Images + ( + `repository`, + `name`, + `pulls`, + `latest_version`, + `version_mask`, + `hidden`, + `unstable`, + `deprecated`, + `deprecation_reason` + ) + VALUES + ( + in_repository, + in_name, + in_pull_count, + in_version, + in_version_mask, + in_hidden, + in_unstable, + in_deprecated, + in_deprecation_reason + ); + + SET out_id = LAST_INSERT_ID(); + SET out_status = 0; + SET out_message = 'OK'; + + ELSE + + UPDATE Images + SET + `name` = in_name, + `pulls` = in_pull_count, + `latest_version` = in_version, + `version_mask` = in_version_mask, + `hidden` = in_hidden, + `unstable` = in_unstable, + `deprecated` = in_deprecated, + `deprecation_reason` = in_deprecation_reason + WHERE + `id` = in_id; + + SET out_id = in_id; + + CALL Image_SavePullHistory(out_id, in_pull_count, out_status, out_message); + + END IF; + +END; +// + +DROP PROCEDURE `Repository_Delete`// +CREATE PROCEDURE `Repository_Delete` +( + in_id INT +) +BEGIN + + DELETE FROM ImagePullHistory WHERE `image_id` IN (SELECT `id` FROM Images WHERE `repository` = in_id); + DELETE FROM Images WHERE `repository` = in_id; + DELETE FROM Repositories WHERE `id` = in_id; + +END; +// + +CREATE PROCEDURE `Image_Delete` +( + in_id INT +) +BEGIN + + DELETE FROM ImagePullHistory WHERE `image_id` = in_id; + DELETE FROM Images WHERE `id` = in_id; + +END; +// \ No newline at end of file From faf428b3e0bc491b83d79a2f2208e7b5d7441591 Mon Sep 17 00:00:00 2001 From: Josh Stark Date: Thu, 25 Apr 2019 12:19:55 +0100 Subject: [PATCH 2/6] Added HOUR to grouping mode for pull history --- .../linuxserver/fleet/model/ImagePullStat.java | 2 +- .../migration/V1.8__PullHistoryAndImageMeta.sql | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java b/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java index a5f1d34..933f7e9 100644 --- a/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java +++ b/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java @@ -50,7 +50,7 @@ public class ImagePullStat { public enum GroupMode { - DAY, WEEK, MONTH, YEAR; + HOUR, DAY, WEEK, MONTH, YEAR; @Override public String toString() { diff --git a/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql b/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql index a5e47c6..27adeca 100644 --- a/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql +++ b/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql @@ -44,11 +44,24 @@ END; CREATE PROCEDURE `Image_GetPullHistory` ( in_image_id INT, - in_grouping_mode ENUM('day', 'week', 'month', 'year') + in_grouping_mode ENUM('hour', 'day', 'week', 'month', 'year') ) BEGIN - IF in_grouping_mode = 'day' THEN + IF in_grouping_mode = 'hour' THEN + + SELECT + `image_id` AS ImageId, + MAX(`pull_count`) AS ImagePulls, + FROM_UNIXTIME(`pull_timestamp`, '%d%m%Y') AS TimeGroup + FROM + ImagePullHistory + WHERE + `image_id` = in_image_id + GROUP BY `image_id`, TimeGroup + ORDER BY TimeGroup; + + ELSEIF in_grouping_mode = 'day' THEN SELECT `image_id` AS ImageId, From c45602840b460237f8424a794f81b3738023522c Mon Sep 17 00:00:00 2001 From: Josh Stark Date: Thu, 25 Apr 2019 12:21:09 +0100 Subject: [PATCH 3/6] Updated time formatting for hour grouping --- .../resources/db/migration/V1.8__PullHistoryAndImageMeta.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql b/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql index 27adeca..da40de0 100644 --- a/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql +++ b/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql @@ -53,7 +53,7 @@ BEGIN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, - FROM_UNIXTIME(`pull_timestamp`, '%d%m%Y') AS TimeGroup + FROM_UNIXTIME(`pull_timestamp`, '%H%d%m%Y') AS TimeGroup FROM ImagePullHistory WHERE From cfecf3f9adae511f7b81d3aa1ccb551bdd1363ff Mon Sep 17 00:00:00 2001 From: Josh Stark Date: Fri, 26 Apr 2019 10:14:11 +0100 Subject: [PATCH 4/6] Added groupMode to API for pull history --- .../linuxserver/fleet/model/ImagePullStat.java | 16 ++++++++++++++++ .../fleet/web/routes/GetImagePullHistoryApi.java | 9 +++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java b/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java index 933f7e9..df4ba0c 100644 --- a/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java +++ b/src/main/java/io/linuxserver/fleet/model/ImagePullStat.java @@ -56,5 +56,21 @@ public class ImagePullStat { public String toString() { return name().toLowerCase(); } + + public static boolean isValid(String value) { + + try { + + if (null == value) { + return false; + } + + valueOf(value); + return true; + + } catch (IllegalArgumentException e) { + return false; + } + } } } 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 c6f4725..e77a6ed 100644 --- a/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java +++ b/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java @@ -46,10 +46,15 @@ public class GetImagePullHistoryApi implements Route { } int imageId = Integer.parseInt(imageIdParam); + List imagePullStats = imageDelegate.fetchImagePullHistory(imageId, getGroupMode(request)); Image image = imageDelegate.fetchImage(imageId); - List imagePullStats = imageDelegate.fetchImagePullHistory(imageId); - return new ApiResponse<>("OK", ApiImagePullHistory.fromPullStats(image, imagePullStats)); } + + private ImagePullStat.GroupMode getGroupMode(Request request) { + + String groupMode = request.params("groupMode"); + return ImagePullStat.GroupMode.isValid(groupMode) ? ImagePullStat.GroupMode.valueOf(groupMode) : ImagePullStat.GroupMode.DAY; + } } From 464b57d6cda50ab96299c3f2458f5cd7cc967c88 Mon Sep 17 00:00:00 2001 From: Josh Stark Date: Fri, 26 Apr 2019 10:26:47 +0100 Subject: [PATCH 5/6] Reversed time group for allow for correct ordering --- .../fleet/web/routes/GetImagePullHistoryApi.java | 2 +- .../db/migration/V1.8__PullHistoryAndImageMeta.sql | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) 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 e77a6ed..bf05b8f 100644 --- a/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java +++ b/src/main/java/io/linuxserver/fleet/web/routes/GetImagePullHistoryApi.java @@ -54,7 +54,7 @@ public class GetImagePullHistoryApi implements Route { private ImagePullStat.GroupMode getGroupMode(Request request) { - String groupMode = request.params("groupMode"); + String groupMode = request.queryParams("groupMode"); return ImagePullStat.GroupMode.isValid(groupMode) ? ImagePullStat.GroupMode.valueOf(groupMode) : ImagePullStat.GroupMode.DAY; } } diff --git a/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql b/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql index da40de0..64a9d5d 100644 --- a/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql +++ b/src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql @@ -51,9 +51,9 @@ BEGIN IF in_grouping_mode = 'hour' THEN SELECT - `image_id` AS ImageId, - MAX(`pull_count`) AS ImagePulls, - FROM_UNIXTIME(`pull_timestamp`, '%H%d%m%Y') AS TimeGroup + `image_id` AS ImageId, + MAX(`pull_count`) AS ImagePulls, + FROM_UNIXTIME(`pull_timestamp`, '%Y%m%d%H') AS TimeGroup FROM ImagePullHistory WHERE @@ -66,7 +66,7 @@ BEGIN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, - FROM_UNIXTIME(`pull_timestamp`, '%d%m%Y') AS TimeGroup + FROM_UNIXTIME(`pull_timestamp`, '%Y%m%d') AS TimeGroup FROM ImagePullHistory WHERE @@ -79,7 +79,7 @@ BEGIN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, - FROM_UNIXTIME(`pull_timestamp`, '%v%Y') AS TimeGroup + FROM_UNIXTIME(`pull_timestamp`, '%Y%v') AS TimeGroup FROM ImagePullHistory WHERE @@ -92,7 +92,7 @@ BEGIN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, - FROM_UNIXTIME(`pull_timestamp`, '%m%Y') AS TimeGroup + FROM_UNIXTIME(`pull_timestamp`, '%Y%m') AS TimeGroup FROM ImagePullHistory WHERE From 9352501b07d47c5d273c76b6c930017d149f3f80 Mon Sep 17 00:00:00 2001 From: Josh Stark Date: Fri, 26 Apr 2019 10:40:53 +0100 Subject: [PATCH 6/6] Small clean up --- .../java/io/linuxserver/fleet/delegate/ImageDelegate.java | 4 ---- .../io/linuxserver/fleet/model/api/ApiImagePullHistory.java | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java b/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java index d6e373d..c0b7b71 100644 --- a/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java +++ b/src/main/java/io/linuxserver/fleet/delegate/ImageDelegate.java @@ -67,10 +67,6 @@ public class ImageDelegate { throw new SaveException(result.getStatusMessage()); } - public List fetchImagePullHistory(int id) { - return fetchImagePullHistory(id, ImagePullStat.GroupMode.DAY); - } - public List fetchImagePullHistory(int id, ImagePullStat.GroupMode groupMode) { return imageDAO.fetchImagePullHistory(id, groupMode); } 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 856c1fc..48b5bb5 100644 --- a/src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java +++ b/src/main/java/io/linuxserver/fleet/model/api/ApiImagePullHistory.java @@ -65,6 +65,7 @@ public class ApiImagePullHistory { private final long pullCount; ApiImagePullStat(String timeGroup, long pullCount) { + this.timeGroup = timeGroup; this.pullCount = pullCount; }