Implement cache pruning, closes #7

This commit is contained in:
Johannes Frohnmeyer 2022-07-31 14:18:49 +02:00
parent cb99626f20
commit 162fdee877
Signed by: Johannes
GPG Key ID: E76429612C2929F4
6 changed files with 126 additions and 19 deletions

View File

@ -16,6 +16,8 @@ import java.nio.file.FileSystem;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Resclone implements ModInitializer, RescloneApi {
public static final Set<PackMetaUnloaded> CONF = new LinkedHashSet<>();
@ -30,6 +32,7 @@ public class Resclone implements ModInitializer, RescloneApi {
public static PackUrlCache urlCache;
public static int packCount = 0;
private boolean pruneUnused = false;
@Override
public void onInitialize() {
@ -104,6 +107,8 @@ public class Resclone implements ModInitializer, RescloneApi {
urlCache.save();
DOWNLOADED_PACKS.clear();
DOWNLOADED_PACKS.addAll(metas);
if (pruneUnused) pruneCache();
}
private Runnable generateTask(PackMetaUnloaded meta, Set<PackMetaLoaded> metas) {
@ -142,6 +147,28 @@ public class Resclone implements ModInitializer, RescloneApi {
};
}
private void pruneCache() {
Set<Path> loadedPacks = DOWNLOADED_PACKS.stream().map(PackMetaLoaded::zipPath).collect(Collectors.toUnmodifiableSet());
Set<Path> toDelete = new HashSet<>();
try (Stream<Path> cacheEntries = Files.list(getConfigPath().resolve("cache"))) {
cacheEntries
.filter(s -> !Files.isRegularFile(s) || !loadedPacks.contains(s))
.forEach(toDelete::add);
} catch (IOException e) {
LOGGER.error("Could find cache entries to prune", e);
}
if (!toDelete.isEmpty()) {
LOGGER.info("Pruning " + toDelete.size() + " unused cache entries");
for (Path path : toDelete) {
try {
Files.delete(path);
} catch (IOException e) {
LOGGER.error("Could not delete unused cache entry: " + path, e);
}
}
}
}
@Override
public Path getConfigPath() {
Path configPath = FabricLoader.getInstance().getConfigDir().resolve("resclone");
@ -154,4 +181,9 @@ public class Resclone implements ModInitializer, RescloneApi {
}
return configPath;
}
@Override
public void setPruneUnused(boolean pruneUnused) {
this.pruneUnused = pruneUnused;
}
}

View File

@ -1,8 +1,9 @@
package io.gitlab.jfronny.resclone;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.resclone.api.RescloneApi;
import io.gitlab.jfronny.resclone.api.RescloneEntry;
import io.gitlab.jfronny.resclone.util.ConfigLoader;
import io.gitlab.jfronny.resclone.util.config.*;
import io.gitlab.jfronny.resclone.fetchers.BasicFileFetcher;
import io.gitlab.jfronny.resclone.fetchers.CurseforgeFetcher;
import io.gitlab.jfronny.resclone.fetchers.GitHubFetcher;
@ -13,6 +14,7 @@ import net.fabricmc.loader.api.FabricLoader;
public class RescloneEntryDefault implements RescloneEntry {
@Override
public void init(RescloneApi api) {
GsonHolder.registerTypeAdapter(RescloneConfig.class, new RescloneConfigTypeAdapter());
api.addFetcher(new BasicFileFetcher());
api.addFetcher(new GitHubFetcher());
api.addFetcher(new CurseforgeFetcher());

View File

@ -16,4 +16,6 @@ public interface RescloneApi {
void reload();
Path getConfigPath();
void setPruneUnused(boolean pruneUnused);
}

View File

@ -1,11 +1,11 @@
package io.gitlab.jfronny.resclone.util;
package io.gitlab.jfronny.resclone.util.config;
import com.google.gson.reflect.TypeToken;
import io.gitlab.jfronny.commons.serialize.gson.api.*;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.api.RescloneApi;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -16,29 +16,25 @@ public class ConfigLoader {
public static void load(RescloneApi api) {
Path configPath = api.getConfigPath().resolve("config.json");
if (!Files.exists(configPath)) {
save(api, new HashSet<>());
save(api, new HashSet<>(), true);
}
try {
StringBuilder text = new StringBuilder();
for (String s : Files.readAllLines(configPath)) {
text.append("\r\n");
text.append(s);
}
Set<PackMetaUnloaded> data = GsonHolder.getGson().fromJson(text.toString(), new TypeToken<Set<PackMetaUnloaded>>(){}.getType());
for (PackMetaUnloaded meta : data) {
try (BufferedReader reader = Files.newBufferedReader(configPath)) {
RescloneConfig data = GsonHolder.getGson().fromJson(reader, RescloneConfig.class);
api.setPruneUnused(data.pruneUnused());
for (PackMetaUnloaded meta : data.packs()) {
api.addPack(meta.fetcher, meta.source, meta.name, meta.forceDownload, meta.forceEnable);
}
if (data.updateRequired()) {
save(api, data.packs(), data.pruneUnused());
}
} catch (IOException e) {
Resclone.LOGGER.error("Could not load config", e);
}
}
public static void save(RescloneApi api, Set<PackMetaUnloaded> data) {
Path configPath = api.getConfigPath().resolve("config.json");
Set<String> text = new HashSet<>();
text.add(GsonHolder.getGson().toJson(data));
try {
Files.write(configPath, text);
public static void save(RescloneApi api, Set<PackMetaUnloaded> packs, boolean pruneUnused) {
try (var writer = Files.newBufferedWriter(api.getConfigPath().resolve("config.json"))) {
GsonHolder.getGson().toJson(new RescloneConfig(packs, pruneUnused), writer);
} catch (IOException e) {
Resclone.LOGGER.error("Could not write config", e);
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.resclone.util.config;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import java.util.Set;
public record RescloneConfig(Set<PackMetaUnloaded> packs, boolean pruneUnused, boolean updateRequired) {
public RescloneConfig(Set<PackMetaUnloaded> packs, boolean pruneUnused) {
this(packs, pruneUnused, false);
}
}

View File

@ -0,0 +1,64 @@
package io.gitlab.jfronny.resclone.util.config;
import com.google.gson.reflect.TypeToken;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.gson.*;
import io.gitlab.jfronny.gson.stream.*;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Set;
public class RescloneConfigTypeAdapter extends TypeAdapter<RescloneConfig> {
private static final String PACKS = "packs";
private static final String PRUNE_UNUSED = "pruneUnused";
private static final Type META_SET = new TypeToken<Set<PackMetaUnloaded>>(){}.getType();
@Override
public RescloneConfig read(JsonReader jsonReader) throws IOException {
if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
// Legacy format compatibility
return new RescloneConfig(GsonHolder.getGson().fromJson(jsonReader, META_SET), false, true);
} else if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
// New format
jsonReader.beginObject();
Set<PackMetaUnloaded> packs = null;
Boolean pruneUnused = null;
boolean updateRequired = false;
while (jsonReader.peek() != JsonToken.END_OBJECT) {
final String name = jsonReader.nextName();
switch (name) {
case PACKS -> {
if (packs != null) throw new JsonParseException("Unexpected duplicate \"" + PACKS + "\" in Resclone config");
packs = GsonHolder.getGson().fromJson(jsonReader, META_SET);
}
case PRUNE_UNUSED -> {
if (pruneUnused != null) throw new JsonParseException("Unexpected duplicate \"" + PRUNE_UNUSED + "\" in Resclone config");
pruneUnused = jsonReader.nextBoolean();
}
default -> throw new JsonParseException("Unexpected element: \"" + name + "\" in Resclone config");
}
}
jsonReader.endObject();
if (packs == null) throw new JsonParseException("Expected Resclone config object to contain packs");
if (pruneUnused == null) {
pruneUnused = true;
updateRequired = true;
}
return new RescloneConfig(packs, pruneUnused, updateRequired);
} else throw new JsonParseException("Expected Resclone config to be an object or array");
}
@Override
public void write(JsonWriter jsonWriter, RescloneConfig rescloneConfig) throws IOException {
jsonWriter.beginObject()
.comment("The packs to be loaded by resclone")
.name(PACKS);
GsonHolder.getGson().toJson(rescloneConfig.packs(), META_SET, jsonWriter);
jsonWriter.comment("Whether to prune unused packs from the cache")
.name(PRUNE_UNUSED)
.value(rescloneConfig.pruneUnused())
.endObject();
}
}