semi-auto cleanup

This commit is contained in:
gnembon committed 2023-02-04 13:26:21 +01:00
1 parent cad80f82c6
commit 8f61271606
99 files changed
+11463 -8007

No files matched your search

@@ -18,6 +18,7 @@ import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.commands.CommandRuntimeException;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.world.level.storage.LevelResource;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
@@ -40,22 +41,28 @@ import org.apache.commons.io.IOUtils;
*/
public class AppStoreManager
{
/** A local copy of the scarpet repo's file structure, to avoid multiple queries to github.com while typing out the
/**
* A local copy of the scarpet repo's file structure, to avoid multiple queries to github.com while typing out the
* {@code /script download} command and getting the suggestions.
*/
private static StoreNode APP_STORE_ROOT = StoreNode.folder(null, "");
/** This is the base link to the scarpet app repo from the github api.
/**
* This is the base link to the scarpet app repo from the github api.
*/
private static String scarpetRepoLink = "https://api.github.com/repos/gnembon/scarpet/contents/programs/";
private static record AppInfo(String name, String url, StoreNode source) {}
private record AppInfo(String name, String url, StoreNode source)
{
}
public static class ScarpetAppStoreValidator extends Validator<String>
{
@Override public String validate(CommandSourceStack source, CarpetRule<String> currentRule, String newValue, String stringInput)
@Override
public String validate(final CommandSourceStack source, final CarpetRule<String> currentRule, String newValue, final String stringInput)
{
if (newValue.equals(currentRule.value())) {
if (newValue.equals(currentRule.value()))
{
// Don't refresh the local repo if it's the same (world change), helps preventing hitting rate limits from github when
// getting suggestions. Pending is a way to invalidate the cache when it gets old, and investigating api usage further
return newValue;
@@ -66,12 +73,19 @@ public class AppStoreManager
scarpetRepoLink = null;
return newValue;
}
if (newValue.endsWith("/")) newValue = newValue.replaceAll("/$", "");
scarpetRepoLink = "https://api.github.com/repos/"+newValue+"/";
if (newValue.endsWith("/"))
{
newValue = newValue.replaceAll("/$", "");
}
scarpetRepoLink = "https://api.github.com/repos/" + newValue + "/";
return newValue;
}
@Override
public String description() { return "Appstore link should point to a valid github repository";}
public String description()
{
return "Appstore link should point to a valid github repository";
}
}
public static class StoreNode
@@ -81,18 +95,19 @@ public class AppStoreManager
public Map<String, StoreNode> children;
public boolean sealed;
public String value;
public static StoreNode folder(StoreNode parent, String name)
public static StoreNode folder(final StoreNode parent, final String name)
{
StoreNode node = new StoreNode(parent, name);
final StoreNode node = new StoreNode(parent, name);
node.children = new HashMap<>();
node.value = null;
node.sealed = false;
return node;
}
public static StoreNode scriptFile(StoreNode parent, String name, String value)
public static StoreNode scriptFile(final StoreNode parent, final String name, final String value)
{
StoreNode node = new StoreNode(parent, name);
final StoreNode node = new StoreNode(parent, name);
node.children = null;
node.value = value;
node.sealed = true;
@@ -103,19 +118,23 @@ public class AppStoreManager
{
return value != null;
}
public String pathElement()
{
return name+(isLeaf()?"":"/");
return name + (isLeaf() ? "" : "/");
}
public String getPath()
{
return createPrePath().toString();
}
private StringBuilder createPrePath()
{
return this == APP_STORE_ROOT ? new StringBuilder() : parent.createPrePath().append(pathElement());
}
private StoreNode(StoreNode parent, String name)
private StoreNode(final StoreNode parent, final String name)
{
this.parent = parent;
this.name = name;
@@ -124,32 +143,38 @@ public class AppStoreManager
public synchronized void fillChildren() throws IOException
{
if (sealed) return;
if (scarpetRepoLink == null) throw new IOException("Accessing scarpet app repo is disabled");
if (sealed)
{
return;
}
if (scarpetRepoLink == null)
{
throw new IOException("Accessing scarpet app repo is disabled");
}
String queryPath = scarpetRepoLink + getPath();
String response;
final String queryPath = scarpetRepoLink + getPath();
final String response;
try
{
response = IOUtils.toString(new URL(queryPath), StandardCharsets.UTF_8);
}
catch (IOException e)
catch (final IOException e)
{
// Not sealing to allow retrying
throw new IOException("Problems fetching " + queryPath, e);
}
JsonArray files = JsonParser.parseString(response).getAsJsonArray();
for(JsonElement je : files)
final JsonArray files = JsonParser.parseString(response).getAsJsonArray();
for (final JsonElement je : files)
{
JsonObject jo = je.getAsJsonObject();
String name = jo.get("name").getAsString();
final JsonObject jo = je.getAsJsonObject();
final String name = jo.get("name").getAsString();
if (jo.get("type").getAsString().equals("dir"))
{
children.put(name, folder(this, name));
}
else// if (name.matches("(\\w+\\.scl?)"))
{
String value = jo.get("download_url").getAsString();
final String value = jo.get("download_url").getAsString();
children.put(name, scriptFile(this, name, value));
}
}
@@ -159,12 +184,16 @@ public class AppStoreManager
/**
* Returns true if doing down the directory structire cannot continue since the matching element is either a leaf or
* a string not matching of any node.
*
* @param pathElement
* @return
*/
public boolean cannotContinueFor(String pathElement) throws IOException
public boolean cannotContinueFor(final String pathElement) throws IOException
{
if (isLeaf()) return true;
if (isLeaf())
{
return true;
}
fillChildren();
return !children.containsKey(pathElement);
}
@@ -173,120 +202,140 @@ public class AppStoreManager
{
if (isLeaf())
{
if (name.endsWith(".sc"))
return Collections.singletonList(getPath());
return Collections.emptyList();
return name.endsWith(".sc") ? Collections.singletonList(getPath()) : Collections.emptyList();
}
fillChildren();
String prefix = getPath();
final String prefix = getPath();
return children.values().stream().
filter(n -> (!n.isLeaf() || n.name.endsWith(".sc"))).
map(s -> prefix+s.pathElement().replaceAll("/$", "")).
map(s -> prefix + s.pathElement().replaceAll("/$", "")).
collect(Collectors.toList());
}
public StoreNode drillDown(String pathElement) throws IOException
public StoreNode drillDown(final String pathElement) throws IOException
{
if (isLeaf()) throw new IOException(pathElement+" is not a folder");
if (isLeaf())
{
throw new IOException(pathElement + " is not a folder");
}
fillChildren();
if (!children.containsKey(pathElement)) throw new IOException("Folder "+pathElement+" is not present");
if (!children.containsKey(pathElement))
{
throw new IOException("Folder " + pathElement + " is not present");
}
return children.get(pathElement);
}
public String getValue(String file) throws IOException
public String getValue(final String file) throws IOException
{
StoreNode leaf = drillDown(file);
if (!leaf.isLeaf()) throw new IOException(file+" is not a file");
final StoreNode leaf = drillDown(file);
if (!leaf.isLeaf())
{
throw new IOException(file + " is not a file");
}
return leaf.value;
}
}
/** This method searches for valid file names from the user-inputted string, e.g if the user has thus far typed
/**
* This method searches for valid file names from the user-inputted string, e.g if the user has thus far typed
* {@code survival/a} then it will return all the files in the {@code survival} directory of the scarpet repo (and
* will automatically highlight those starting with a), and the string {@code survival/} as the current most valid path.
*
* @param currentPath The path down which we want to search for files
* @return A pair of the current valid path, as well as the set of all the file/directory names at the end of that path
*/
public static List<String> suggestionsFromPath(String currentPath) throws IOException
public static List<String> suggestionsFromPath(final String currentPath) throws IOException
{
String[] path = currentPath.split("/");
final String[] path = currentPath.split("/");
StoreNode appKiosk = APP_STORE_ROOT;
for(String pathElement : path)
for (final String pathElement : path)
{
if (appKiosk.cannotContinueFor(pathElement)) break;
if (appKiosk.cannotContinueFor(pathElement))
{
break;
}
appKiosk = appKiosk.children.get(pathElement);
}
List<String> filteredSuggestions = appKiosk.createPathSuggestions().stream().filter(s -> s.startsWith(currentPath)).collect(Collectors.toList());
if (filteredSuggestions.size() == 1) {
final List<String> filteredSuggestions = appKiosk.createPathSuggestions().stream().filter(s -> s.startsWith(currentPath)).collect(Collectors.toList());
if (filteredSuggestions.size() == 1)
{
if (!appKiosk.isLeaf())
{
return suggestionsFromPath(filteredSuggestions.get(0)); // Start suggesting directory contents
}
}
return filteredSuggestions;
}
/** Downloads script and saves it to appropriate place.
/**
* Downloads script and saves it to appropriate place.
*
* @param path The user-inputted path to the script
* @return {@code 1} if we succesfully saved the script, {@code 0} otherwise
*/
public static int downloadScript(CommandSourceStack source, String path)
public static int downloadScript(final CommandSourceStack source, final String path)
{
AppInfo nodeInfo = getFileNode(path);
final AppInfo nodeInfo = getFileNode(path);
return downloadScript(source, path, nodeInfo, false);
}
private static int downloadScript(CommandSourceStack source, String path, AppInfo nodeInfo, boolean useTrash)
private static int downloadScript(final CommandSourceStack source, final String path, final AppInfo nodeInfo, final boolean useTrash)
{
String code;
final String code;
try
{
code = IOUtils.toString(new URL(nodeInfo.url()), StandardCharsets.UTF_8);
}
catch (IOException e)
catch (final IOException e)
{
throw new CommandRuntimeException(Messenger.c("rb Failed to obtain app file content: "+e.getMessage()));
throw new CommandRuntimeException(Messenger.c("rb Failed to obtain app file content: " + e.getMessage()));
}
if (!saveScriptToFile(source, path, nodeInfo.name(), code, false, useTrash)) return 0;
boolean success = CarpetServer.scriptServer.addScriptHost(source, nodeInfo.name().replaceFirst("\\.sc$", ""), null, true, false, false, nodeInfo.source());
return success?1:0;
if (!saveScriptToFile(source, path, nodeInfo.name(), code, false, useTrash))
{
return 0;
}
final boolean success = CarpetServer.scriptServer.addScriptHost(source, nodeInfo.name().replaceFirst("\\.sc$", ""), null, true, false, false, nodeInfo.source());
return success ? 1 : 0;
}
/** Gets the code once the user inputs the command.
/**
* Gets the code once the user inputs the command.
*
* @param appPath The user inputted path to the scarpet script
* @return Pair of app file name and content
*/
public static AppInfo getFileNode(String appPath)
public static AppInfo getFileNode(final String appPath)
{
return getFileNodeFrom(APP_STORE_ROOT, appPath);
}
public static AppInfo getFileNodeFrom(StoreNode start, String appPath)
public static AppInfo getFileNodeFrom(final StoreNode start, final String appPath)
{
String[] path = appPath.split("/");
final String[] path = appPath.split("/");
StoreNode appKiosk = start;
try
{
for(String pathElement : Arrays.copyOfRange(path, 0, path.length-1))
for (final String pathElement : Arrays.copyOfRange(path, 0, path.length - 1))
{
appKiosk = appKiosk.drillDown(pathElement);
String appName = path[path.length-1];
}
final String appName = path[path.length - 1];
appKiosk.getValue(appName);
return new AppInfo(appName, appKiosk.getValue(appName), appKiosk);
}
catch (IOException e)
catch (final IOException e)
{
throw new CommandRuntimeException(Messenger.c("rb '"+ appPath + "' is not a valid path to a scarpet app: "+e.getMessage()));
throw new CommandRuntimeException(Messenger.c("rb '" + appPath + "' is not a valid path to a scarpet app: " + e.getMessage()));
}
}
public static boolean saveScriptToFile(CommandSourceStack source, String path, String appFileName, String code, boolean globalSavePath, boolean useTrash)
public static boolean saveScriptToFile(final CommandSourceStack source, final String path, final String appFileName, final String code, final boolean globalSavePath, final boolean useTrash)
{
Path scriptLocation;
final Path scriptLocation;
if (globalSavePath && !source.getServer().isDedicatedServer()) // never happens, this is always called with globalSavePath being false
{ //cos config folder only is in clients
scriptLocation = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts/appstore").toAbsolutePath().resolve(path);
@@ -307,29 +356,29 @@ public class AppStoreManager
int i = 0;
while (Files.exists(trashPath))
{
String[] nameAndExtension = appFileName.split("\\.");
String newFileName = String.format(nameAndExtension[0] + "%02d." + nameAndExtension[1],i);
final String[] nameAndExtension = appFileName.split("\\.");
final String newFileName = String.format(nameAndExtension[0] + "%02d." + nameAndExtension[1], i);
trashPath = trashPath.getParent().resolve(newFileName);
i++;
}
Files.move(scriptLocation, trashPath);
}
Messenger.m(source, String.format("gi Note: replaced existing app '%s'"+(useTrash ? " (old moved to /trash folder)" : ""), appFileName));
Messenger.m(source, String.format("gi Note: replaced existing app '%s'" + (useTrash ? " (old moved to /trash folder)" : ""), appFileName));
}
BufferedWriter writer = Files.newBufferedWriter(scriptLocation);
final BufferedWriter writer = Files.newBufferedWriter(scriptLocation);
writer.write(code);
writer.close();
}
catch (IOException e)
catch (final IOException e)
{
Messenger.m(source, "r Error while downloading app: " + e.toString());
Messenger.m(source, "r Error while downloading app: " + e);
CarpetScriptServer.LOG.warn("Error while downloading app", e);
return false;
}
return true;
}
public static void writeUrlToFile(String url, Path destination) throws IOException
public static void writeUrlToFile(final String url, final Path destination) throws IOException
{
try (final InputStream in = new URL(url).openStream())
{
@@ -337,88 +386,112 @@ public class AppStoreManager
}
}
private static String getFullContentUrl(String original, StoreNode storeSource)
private static String getFullContentUrl(final String original, final StoreNode storeSource)
{
if (original.matches("^https?://.*$")) // We've got a full url here: Just use it
{
return original;
}
if (original.charAt(0) == '/') // We've got an absolute path: Use app store root
{
return getFileNode(original.substring(1)).url();
}
return getFileNodeFrom(storeSource, original).url(); // Relative path: Use download location
}
public static void addResource(CarpetScriptHost carpetScriptHost, StoreNode storeSource, Value resource)
public static void addResource(final CarpetScriptHost carpetScriptHost, final StoreNode storeSource, final Value resource)
{
if (!(resource instanceof MapValue))
throw new InternalExpressionException("This is not a valid resource map: "+resource.getString());
Map<String, Value> resourceMap = ((MapValue) resource).getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue));
if (!resourceMap.containsKey("source")) throw new InternalExpressionException("Missing 'source' field in resource descriptor: "+resource.getString());
String source = resourceMap.get("source").getString();
String contentUrl = getFullContentUrl(source, storeSource);
String target = resourceMap.computeIfAbsent("target", k -> new StringValue(contentUrl.substring(contentUrl.lastIndexOf('/') + 1))).getString();
boolean shared = resourceMap.getOrDefault("shared", Value.FALSE).getBoolean();
if (!(resource instanceof final MapValue map))
{
throw new InternalExpressionException("This is not a valid resource map: " + resource.getString());
}
final Map<String, Value> resourceMap = map.getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue));
if (!resourceMap.containsKey("source"))
{
throw new InternalExpressionException("Missing 'source' field in resource descriptor: " + resource.getString());
}
final String source = resourceMap.get("source").getString();
final String contentUrl = getFullContentUrl(source, storeSource);
final String target = resourceMap.computeIfAbsent("target", k -> new StringValue(contentUrl.substring(contentUrl.lastIndexOf('/') + 1))).getString();
final boolean shared = resourceMap.getOrDefault("shared", Value.FALSE).getBoolean();
if (!carpetScriptHost.applyActionForResource(target, shared, p -> {
try {
try
{
writeUrlToFile(contentUrl, p);
} catch (IOException e) {
throw new InternalExpressionException("Unable to write resource "+target+": "+e.toString());
}
catch (final IOException e)
{
throw new InternalExpressionException("Unable to write resource " + target + ": " + e);
}
}))
{
throw new InternalExpressionException("Unable to write resource "+target);
throw new InternalExpressionException("Unable to write resource " + target);
}
CarpetScriptServer.LOG.info("Downloaded resource "+target+" from "+contentUrl);
CarpetScriptServer.LOG.info("Downloaded resource " + target + " from " + contentUrl);
}
/**
* Gets a new StoreNode for an app's dependency with proper relativeness. Will be null if it comes from an external URL
*
* @param originalSource The StoreNode from the container's app
* @param sourceString The string the app specified as source
* @param contentUrl The full content URL, from {@link #getFullContentUrl(String, StoreNode)}
* @param sourceString The string the app specified as source
* @param contentUrl The full content URL, from {@link #getFullContentUrl(String, StoreNode)}
* @return A {@link StoreNode} that can be used in an app that came from the provided source
*/
private static StoreNode getNewStoreNode(StoreNode originalSource, String sourceString, String contentUrl)
private static StoreNode getNewStoreNode(final StoreNode originalSource, String sourceString, final String contentUrl)
{
StoreNode next = originalSource;
if (sourceString == contentUrl) // External URL (check getFullUrlContent)
{
return null;
}
if (sourceString.charAt(0) == '/') // Absolute URL
{
next = APP_STORE_ROOT;
sourceString = sourceString.substring(1);
}
String[] dirs = sourceString.split("/");
final String[] dirs = sourceString.split("/");
try
{
for (int i = 0; i < dirs.length - 1; i++)
{
next = next.drillDown(dirs[i]);
}
}
catch (IOException e)
catch (final IOException e)
{
return null; // Should never happen, but let's not give a potentially incorrect node just in case
}
return next;
}
public static void addLibrary(CarpetScriptHost carpetScriptHost, StoreNode storeSource, Value library) {
if (!(library instanceof MapValue))
throw new InternalExpressionException("This is not a valid library map: "+library.getString());
Map<String, String> libraryMap = ((MapValue) library).getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), e -> e.getValue().getString()));
String source = libraryMap.get("source");
String contentUrl = getFullContentUrl(source, storeSource);
String target = libraryMap.computeIfAbsent("target", k -> contentUrl.substring(contentUrl.lastIndexOf('/') + 1));
public static void addLibrary(final CarpetScriptHost carpetScriptHost, final StoreNode storeSource, final Value library)
{
if (!(library instanceof final MapValue map))
{
throw new InternalExpressionException("This is not a valid library map: " + library.getString());
}
final Map<String, String> libraryMap = map.getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), e -> e.getValue().getString()));
final String source = libraryMap.get("source");
final String contentUrl = getFullContentUrl(source, storeSource);
final String target = libraryMap.computeIfAbsent("target", k -> contentUrl.substring(contentUrl.lastIndexOf('/') + 1));
if (!(contentUrl.endsWith(".sc") || contentUrl.endsWith(".scl")))
{
throw new InternalExpressionException("App resource type must download a scarpet app or library.");
}
if (target.indexOf('/') != -1)
{
throw new InternalExpressionException("App resource tried to leave script reserved space");
}
try
{
downloadScript(carpetScriptHost.responsibleSource, target, new AppInfo(target, contentUrl, getNewStoreNode(storeSource, source, contentUrl)), true);
}
catch (CommandRuntimeException e)
catch (final CommandRuntimeException e)
{
throw new InternalExpressionException("Error when installing app dependencies: " + e.toString());
throw new InternalExpressionException("Error when installing app dependencies: " + e);
}
CarpetScriptServer.LOG.info("Downloaded app "+target+" from "+contentUrl);
CarpetScriptServer.LOG.info("Downloaded app " + target + " from " + contentUrl);
}
}