Skip to content

Commit b76e583

Browse files
authored
cf: cache search mod operations (#501)
* cf: cache search mod operations * Remove old failed request catch point
1 parent 81c9e10 commit b76e583

File tree

5 files changed

+81
-54
lines changed

5 files changed

+81
-54
lines changed

src/main/java/me/itzg/helpers/curseforge/CurseForgeApiClient.java

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.net.URI;
77
import java.nio.file.Files;
88
import java.nio.file.Path;
9+
import java.time.Duration;
910
import java.util.Collection;
1011
import java.util.HashMap;
1112
import java.util.Map;
@@ -22,10 +23,12 @@
2223
import me.itzg.helpers.curseforge.model.ModsSearchResponse;
2324
import me.itzg.helpers.errors.GenericException;
2425
import me.itzg.helpers.errors.InvalidParameterException;
26+
import me.itzg.helpers.errors.RateLimitException;
2527
import me.itzg.helpers.http.FailedRequestException;
2628
import me.itzg.helpers.http.Fetch;
2729
import me.itzg.helpers.http.FileDownloadStatusHandler;
2830
import me.itzg.helpers.http.SharedFetch;
31+
import me.itzg.helpers.http.SharedFetch.Options;
2932
import me.itzg.helpers.http.UriBuilder;
3033
import me.itzg.helpers.json.ObjectMappers;
3134
import org.slf4j.Logger;
@@ -41,18 +44,26 @@ public class CurseForgeApiClient implements AutoCloseable {
4144
public static final String CATEGORY_MC_MODS = "mc-mods";
4245
public static final String CATEGORY_BUKKIT_PLUGINS = "bukkit-plugins";
4346
public static final String CATEGORY_WORLDS = "worlds";
47+
public static final String API_KEY_VAR = "CF_API_KEY";
48+
public static final String ETERNAL_DEVELOPER_CONSOLE_URL = "https://console.curseforge.com/";
4449

4550
private static final String API_KEY_HEADER = "x-api-key";
4651
static final String MINECRAFT_GAME_ID = "432";
4752

53+
public static final String OP_SEARCH_MOD_WITH_GAME_ID_SLUG_CLASS_ID = "searchModWithGameIdSlugClassId";
54+
private static final Map<String, Duration> CACHE_DURATIONS = new HashMap<>();
55+
static {
56+
CACHE_DURATIONS.put(OP_SEARCH_MOD_WITH_GAME_ID_SLUG_CLASS_ID, Duration.ofHours(1));
57+
}
58+
4859
private final SharedFetch preparedFetch;
4960
private final UriBuilder uriBuilder;
5061
private final UriBuilder downloadFallbackUriBuilder;
5162
private final String gameId;
5263

5364
private final ApiCaching apiCaching;
5465

55-
public CurseForgeApiClient(String apiBaseUrl, String apiKey, SharedFetch.Options sharedFetchOptions, String gameId,
66+
public CurseForgeApiClient(String apiBaseUrl, String apiKey, Options sharedFetchOptions, String gameId,
5667
ApiCaching apiCaching
5768
) {
5869
this.apiCaching = apiCaching;
@@ -61,7 +72,7 @@ public CurseForgeApiClient(String apiBaseUrl, String apiKey, SharedFetch.Options
6172
}
6273

6374
this.preparedFetch = Fetch.sharedFetch("install-curseforge",
64-
(sharedFetchOptions != null ? sharedFetchOptions : SharedFetch.Options.builder().build())
75+
(sharedFetchOptions != null ? sharedFetchOptions : Options.builder().build())
6576
.withHeader(API_KEY_HEADER, apiKey.trim())
6677
);
6778
this.uriBuilder = UriBuilder.withBaseUrl(apiBaseUrl);
@@ -87,6 +98,10 @@ static FileDownloadStatusHandler modFileDownloadStatusHandler(Path outputDir, Lo
8798
};
8899
}
89100

101+
public static Map<String, Duration> getCacheDurations() {
102+
return CACHE_DURATIONS;
103+
}
104+
90105
@Override
91106
public void close() {
92107
preparedFetch.close();
@@ -111,28 +126,34 @@ Mono<CategoryInfo> loadCategoryInfo(Collection<String> applicableClassIdSlugs) {
111126

112127
return Mono.just(new CategoryInfo(contentClassIds, slugIds));
113128
}
114-
);
129+
)
130+
.onErrorMap(FailedRequestException::isForbidden, this::errorMapForbidden);
115131
}
116132

117133
Mono<CurseForgeMod> searchMod(String slug, int classId) {
118-
return preparedFetch.fetch(
119-
uriBuilder.resolve("/v1/mods/search?gameId={gameId}&slug={slug}&classId={classId}",
120-
gameId, slug, classId
121-
)
122-
)
123-
.toObject(ModsSearchResponse.class)
124-
.assemble()
125-
.flatMap(searchResponse -> {
126-
if (searchResponse.getData() == null || searchResponse.getData().isEmpty()) {
127-
return Mono.error(new GenericException("No mods found with slug=" + slug));
128-
}
129-
else if (searchResponse.getData().size() > 1) {
130-
return Mono.error(new GenericException("More than one mod found with slug=" + slug));
131-
}
132-
else {
133-
return Mono.just(searchResponse.getData().get(0));
134-
}
135-
});
134+
return
135+
apiCaching.cache(OP_SEARCH_MOD_WITH_GAME_ID_SLUG_CLASS_ID, CurseForgeMod.class,
136+
preparedFetch.fetch(
137+
uriBuilder.resolve("/v1/mods/search?gameId={gameId}&slug={slug}&classId={classId}",
138+
gameId, slug, classId
139+
)
140+
)
141+
.toObject(ModsSearchResponse.class)
142+
.assemble()
143+
.flatMap(searchResponse -> {
144+
if (searchResponse.getData() == null || searchResponse.getData().isEmpty()) {
145+
return Mono.error(new GenericException("No mods found with slug=" + slug));
146+
}
147+
else if (searchResponse.getData().size() > 1) {
148+
return Mono.error(new GenericException("More than one mod found with slug=" + slug));
149+
}
150+
else {
151+
return Mono.just(searchResponse.getData().get(0));
152+
}
153+
})
154+
.onErrorMap(FailedRequestException::isForbidden, this::errorMapForbidden),
155+
gameId, slug, classId
156+
);
136157
}
137158

138159
/**
@@ -179,7 +200,8 @@ Mono<Integer> slugToId(CategoryInfo categoryInfo,
179200
.findFirst()
180201
.map(CurseForgeMod::getId)
181202
.orElseThrow(() -> new GenericException("Unable to resolve slug into ID (no matches): " + slug))
182-
);
203+
)
204+
.onErrorMap(FailedRequestException::isForbidden, this::errorMapForbidden);
183205
}
184206

185207
public Mono<CurseForgeMod> getModInfo(
@@ -193,6 +215,7 @@ public Mono<CurseForgeMod> getModInfo(
193215
)
194216
.toObject(GetModResponse.class)
195217
.assemble()
218+
.onErrorMap(FailedRequestException::isForbidden, this::errorMapForbidden)
196219
.checkpoint("Getting mod info for " + projectID)
197220
.map(GetModResponse::getData),
198221
projectID
@@ -219,6 +242,7 @@ public Mono<CurseForgeFile> getModFileInfo(
219242
}
220243
return e;
221244
})
245+
.onErrorMap(FailedRequestException::isForbidden, this::errorMapForbidden)
222246
.map(GetModFileResponse::getData)
223247
.checkpoint(),
224248
projectID, fileID
@@ -285,4 +309,21 @@ private static URI normalizeDownloadUrl(String downloadUrl) {
285309
);
286310
}
287311

312+
public Throwable errorMapForbidden(Throwable throwable) {
313+
final FailedRequestException e = (FailedRequestException) throwable;
314+
315+
log.debug("Failed request details: {}", e.toString());
316+
317+
if (e.getBody().contains("There might be too much traffic")) {
318+
return new RateLimitException(null, String.format("Access to %s has been rate-limited.", uriBuilder.getBaseUrl()), e);
319+
}
320+
else {
321+
return new InvalidParameterException(String.format("Access to %s is forbidden or rate-limit has been exceeded."
322+
+ " Ensure %s is set to a valid API key from %s or allow rate-limit to reset.",
323+
uriBuilder.getBaseUrl(), API_KEY_VAR, ETERNAL_DEVELOPER_CONSOLE_URL
324+
), e
325+
);
326+
}
327+
328+
}
288329
}

src/main/java/me/itzg/helpers/curseforge/CurseForgeFilesCommand.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ public void setSlugCategory(String defaultCategory) {
6868
+ "%nCan also be passed via CF_API_BASE_URL")
6969
String apiBaseUrl;
7070

71-
@Option(names = "--api-key", defaultValue = "${env:" + CurseForgeInstaller.API_KEY_VAR + "}",
71+
@Option(names = "--api-key", defaultValue = "${env:" + API_KEY_VAR + "}",
7272
description = "An API key allocated from the Eternal developer console at "
73-
+ CurseForgeInstaller.ETERNAL_DEVELOPER_CONSOLE_URL +
74-
"%nCan also be passed via " + CurseForgeInstaller.API_KEY_VAR
73+
+ ETERNAL_DEVELOPER_CONSOLE_URL +
74+
"%nCan also be passed via " + API_KEY_VAR
7575
)
7676
String apiKey;
7777

@@ -117,7 +117,8 @@ public Integer call() throws Exception {
117117
if (modFileRefs != null && !modFileRefs.isEmpty()) {
118118
try (
119119
final ApiCaching apiCaching = disableApiCaching ? new ApiCachingDisabled()
120-
: new ApiCachingImpl(outputDir, CACHING_NAMESPACE, cacheArgs);
120+
: new ApiCachingImpl(outputDir, CACHING_NAMESPACE, cacheArgs)
121+
.setCacheDurations(CurseForgeApiClient.getCacheDurations());
121122
final CurseForgeApiClient apiClient = new CurseForgeApiClient(
122123
apiBaseUrl, apiKey, sharedFetchArgs.options(),
123124
CurseForgeApiClient.MINECRAFT_GAME_ID,

src/main/java/me/itzg/helpers/curseforge/CurseForgeInstaller.java

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
import me.itzg.helpers.curseforge.model.ModLoader;
4646
import me.itzg.helpers.errors.GenericException;
4747
import me.itzg.helpers.errors.InvalidParameterException;
48-
import me.itzg.helpers.errors.RateLimitException;
4948
import me.itzg.helpers.fabric.FabricLauncherInstaller;
5049
import me.itzg.helpers.files.Manifests;
5150
import me.itzg.helpers.files.ResultsFileWriter;
@@ -71,10 +70,8 @@
7170
@Slf4j
7271
public class CurseForgeInstaller {
7372

74-
public static final String API_KEY_VAR = "CF_API_KEY";
7573
public static final String MODPACK_ZIP_VAR = "CF_MODPACK_ZIP";
7674

77-
public static final String ETERNAL_DEVELOPER_CONSOLE_URL = "https://console.curseforge.com/";
7875
public static final String CURSEFORGE_ID = "curseforge";
7976
public static final String REPO_SUBDIR_MODPACKS = "modpacks";
8077
public static final String REPO_SUBDIR_MODS = "mods";
@@ -199,19 +196,20 @@ void install(String slug, InstallationEntryPoint entryPoint) {
199196
log.warn("API key is not set, so will re-use previous modpack installation of {}",
200197
manifest.getSlug() != null ? manifest.getSlug() : "Project ID " + manifest.getModId()
201198
);
202-
log.warn("Obtain an API key from " + ETERNAL_DEVELOPER_CONSOLE_URL
203-
+ " and set the environment variable " + API_KEY_VAR + " in order to restore full functionality.");
199+
log.warn("Obtain an API key from " + CurseForgeApiClient.ETERNAL_DEVELOPER_CONSOLE_URL
200+
+ " and set the environment variable " + CurseForgeApiClient.API_KEY_VAR + " in order to restore full functionality.");
204201
return;
205202
}
206203
else {
207-
throw new InvalidParameterException("API key is not set. Obtain an API key from " + ETERNAL_DEVELOPER_CONSOLE_URL
208-
+ " and set the environment variable " + API_KEY_VAR);
204+
throw new InvalidParameterException("API key is not set. Obtain an API key from " + CurseForgeApiClient.ETERNAL_DEVELOPER_CONSOLE_URL
205+
+ " and set the environment variable " + CurseForgeApiClient.API_KEY_VAR);
209206
}
210207
}
211208

212209
try (
213210
final ApiCaching apiCaching = disableApiCaching ? new ApiCachingDisabled()
214-
: new ApiCachingImpl(outputDir, CACHING_NAMESPACE, cacheArgs);
211+
: new ApiCachingImpl(outputDir, CACHING_NAMESPACE, cacheArgs)
212+
.setCacheDurations(CurseForgeApiClient.getCacheDurations());
215213
final CurseForgeApiClient cfApi = new CurseForgeApiClient(
216214
apiBaseUrl, apiKey, sharedFetchOptions,
217215
CurseForgeApiClient.MINECRAFT_GAME_ID,
@@ -230,23 +228,6 @@ void install(String slug, InstallationEntryPoint entryPoint) {
230228
new InstallContext(slug, cfApi, categoryInfo, manifest)
231229
);
232230

233-
} catch (FailedRequestException e) {
234-
if (e.getStatusCode() == 403) {
235-
log.debug("Failed request details: {}", e.toString());
236-
237-
if (e.getBody().contains("There might be too much traffic")) {
238-
throw new RateLimitException(null, String.format("Access to %s has been rate-limited.", apiBaseUrl), e);
239-
}
240-
else {
241-
throw new InvalidParameterException(String.format("Access to %s is forbidden or rate-limit has been exceeded."
242-
+ " Ensure %s is set to a valid API key from %s or allow rate-limit to reset.",
243-
apiBaseUrl, API_KEY_VAR, ETERNAL_DEVELOPER_CONSOLE_URL
244-
), e);
245-
}
246-
}
247-
else {
248-
throw e;
249-
}
250231
} catch (IOException e) {
251232
throw new GenericException("Failed to setup API caching", e);
252233
}

src/main/java/me/itzg/helpers/curseforge/InstallCurseForgeCommand.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ public class InstallCurseForgeCommand implements Callable<Integer> {
7979
description = "Allows for overriding the CurseForge Eternal API used")
8080
String apiBaseUrl;
8181

82-
@Option(names = "--api-key", defaultValue = "${env:" + CurseForgeInstaller.API_KEY_VAR + "}",
82+
@Option(names = "--api-key", defaultValue = "${env:" + CurseForgeApiClient.API_KEY_VAR + "}",
8383
description = "An API key allocated from the Eternal developer console at "
84-
+ CurseForgeInstaller.ETERNAL_DEVELOPER_CONSOLE_URL +
85-
"%nCan also be passed via " + CurseForgeInstaller.API_KEY_VAR
84+
+ CurseForgeApiClient.ETERNAL_DEVELOPER_CONSOLE_URL +
85+
"%nCan also be passed via " + CurseForgeApiClient.API_KEY_VAR
8686
)
8787
String apiKey;
8888

src/main/java/me/itzg/helpers/http/FailedRequestException.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public static boolean isNotFound(Throwable throwable) {
3232
return isStatus(throwable, HttpResponseStatus.NOT_FOUND);
3333
}
3434

35+
public static boolean isForbidden(Throwable throwable) {
36+
return isStatus(throwable, HttpResponseStatus.FORBIDDEN);
37+
}
38+
3539
public static boolean isStatus(Throwable throwable, HttpResponseStatus... statuses) {
3640
if (throwable instanceof FailedRequestException) {
3741
final int actualStatus = ((FailedRequestException) throwable).getStatusCode();

0 commit comments

Comments
 (0)