hotfix GIT subscription + local FS + script invocation feature

This commit is contained in:
kirillius 2025-10-08 15:24:25 +03:00
parent 7337ab7021
commit aff02e11f6
7 changed files with 170 additions and 80 deletions

View File

@ -59,7 +59,7 @@ public class App implements Context, Closeable {
} catch (IOException e) { } catch (IOException e) {
loadedConfig = new Config(); loadedConfig = new Config();
loadedConfig.setSubscriptions(new ArrayList<>(List.of( loadedConfig.setSubscriptions(new ArrayList<>(List.of(
new RepositoryConfig("updates", GitSubscription.class, "https://git.kirillius.ru/kirillius/protected-resources-list.git") new RepositoryConfig("updates", GitSubscription.class, "https://git.kirillius.ru/kirillius/protected-resources-list.git", "")
))); )));
try { try {
Config.store(loadedConfig, launcherConfig.getConfigFile()); Config.store(loadedConfig, launcherConfig.getConfigFile());

View File

@ -3,9 +3,6 @@ package ru.kirillius.pf.sdn.External.API;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.json.JSONObject;
import org.json.JSONTokener;
import ru.kirillius.json.JSONUtility;
import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig; import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
@ -14,14 +11,16 @@ import ru.kirillius.pf.sdn.core.Util.HashUtil;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.Collections;
import java.util.Map;
import java.util.StringJoiner;
/** /**
* Subscription provider that pulls JSON resource bundles from a Git repository cache. * Subscription provider that pulls JSON resource bundles from a Git repository cache.
*/ */
public class GitSubscription implements SubscriptionProvider { public class GitSubscription implements SubscriptionProvider {
private final static String CTX = GitSubscription.class.getSimpleName();
private final Context context; private final Context context;
/** /**
@ -31,90 +30,43 @@ public class GitSubscription implements SubscriptionProvider {
this.context = context; this.context = context;
} }
/**
* Clones or updates the configured repository and loads resource bundles from the {@code resources} directory.
*/
@Override
public Map<String, NetworkResourceBundle> getResources(RepositoryConfig config) {
try {
var repoDir = new File(context.getConfig().getCacheDirectory(), "git/" + HashUtil.md5(config.getName()));
if (!repoDir.exists()) {
if (!repoDir.mkdirs()) {
throw new IOException("Unable to create directory: " + repoDir.getAbsolutePath());
}
}
var existing = isGitRepository(repoDir);
var repository = existing ? openRepository(repoDir) : cloneRepository(config.getSource(), repoDir);
if (existing) {
SystemLogger.message("Fetching git repository " + config.getName() + " (" + config.getSource() + ")", CTX);
checkAndPullUpdates(repository);
}
repository.close();
var resourcesDir = new File(repoDir, "resources");
if (!resourcesDir.exists()) {
SystemLogger.error(resourcesDir + " is not exist in repo (" + config.getSource() + ")", CTX);
return Collections.emptyMap();
}
var map = new HashMap<String, NetworkResourceBundle>();
for (var file : Objects.requireNonNull(resourcesDir.listFiles())) {
try (var stream = new FileInputStream(file)) {
var name = file.getName();
if (!name.endsWith(".json")) {
continue;
}
var bundle = JSONUtility.deserializeStructure(new JSONObject(new JSONTokener(stream)), NetworkResourceBundle.class);
map.put(name.substring(0, name.length() - 5), bundle);
}
}
return map;
} catch (Exception e) {
SystemLogger.error("Error while reading git repository", CTX, e);
throw new RuntimeException(e);
}
}
/** /**
* Runs {@code git fetch} and {@code git pull} when remote updates are available. * Runs {@code git fetch} and {@code git pull} when remote updates are available.
*/ */
private static void checkAndPullUpdates(Git git) throws GitAPIException { private static void checkAndPullUpdates(Git git) {
try {
var fetchResult = git.fetch()
.setCheckFetchedObjects(true)
.call();
var fetchResult = git.fetch() if (fetchResult.getTrackingRefUpdates() != null && !fetchResult.getTrackingRefUpdates().isEmpty()) {
.setCheckFetchedObjects(true) SystemLogger.message("Downloading updates...", CTX);
.call(); var pullResult = git.pull().call();
if (fetchResult.getTrackingRefUpdates() != null && !fetchResult.getTrackingRefUpdates().isEmpty()) { if (pullResult.isSuccessful()) {
SystemLogger.message("Downloading updates...", CTX); SystemLogger.message("Git pull is successful", CTX);
var pullResult = git.pull().call();
if (pullResult.isSuccessful()) { // Проверяем были ли обновлены файлы
SystemLogger.message("Git pull is successful", CTX); if (pullResult.getFetchResult() != null && !pullResult.getFetchResult().getTrackingRefUpdates().isEmpty()) {
var updatedFiles = new StringJoiner("\n");
// Проверяем были ли обновлены файлы pullResult.getFetchResult().getTrackingRefUpdates().forEach(refUpdate -> updatedFiles.add(" - " + refUpdate.getLocalName() +
if (pullResult.getFetchResult() != null && !pullResult.getFetchResult().getTrackingRefUpdates().isEmpty()) { " : " + refUpdate.getOldObjectId().abbreviate(7).name() +
var updatedFiles = new StringJoiner("\n"); " -> " + refUpdate.getNewObjectId().abbreviate(7).name()));
pullResult.getFetchResult().getTrackingRefUpdates().forEach(refUpdate -> updatedFiles.add(" - " + refUpdate.getLocalName() + SystemLogger.message("Updated files: " + updatedFiles, CTX);
" : " + refUpdate.getOldObjectId().abbreviate(7).name() + }
" -> " + refUpdate.getNewObjectId().abbreviate(7).name())); } else {
SystemLogger.message("Updated files: " + updatedFiles, CTX); SystemLogger.error("Download failed", CTX);
} }
} else { } else {
SystemLogger.error("Download failed", CTX); SystemLogger.message("There is no updates", CTX);
} }
} else { } catch (Exception e) {
SystemLogger.message("There is no updates", CTX); SystemLogger.error("Failed to fetch & pull repository", CTX, e);
} }
} }
private final static String CTX = GitSubscription.class.getSimpleName();
/** /**
* Clones the repository into the given path. * Clones the repository into the given path.
*/ */
@ -150,4 +102,49 @@ public class GitSubscription implements SubscriptionProvider {
var gitDir = new File(directory, ".git"); var gitDir = new File(directory, ".git");
return gitDir.exists() && gitDir.isDirectory(); return gitDir.exists() && gitDir.isDirectory();
} }
/**
* Clones or updates the configured repository and loads resource bundles from the {@code resources} directory.
*/
@Override
public Map<String, NetworkResourceBundle> getResources(RepositoryConfig config) {
try {
var repoDir = new File(context.getConfig().getCacheDirectory(), "git/" + HashUtil.md5(config.getName()));
if (!repoDir.exists()) {
if (!repoDir.mkdirs()) {
throw new IOException("Unable to create directory: " + repoDir.getAbsolutePath());
}
}
var existing = isGitRepository(repoDir);
var repository = existing ? openRepository(repoDir) : cloneRepository(config.getSource(), repoDir);
if (existing) {
SystemLogger.message("Fetching git repository " + config.getName() + " (" + config.getSource() + ")", CTX);
checkAndPullUpdates(repository);
}
repository.close();
if(!config.getScript().isBlank()){
try (var shell = new ShellExecutor(ShellExecutor.Config.builder().useSSH(false).build())) {
var result = shell.executeCommand(new String[]{
config.getScript(),
config.getSource()
});
SystemLogger.message("Shell command execution result:"+result, CTX);
}
}
var resourcesDir = new File(repoDir, "resources");
if (!resourcesDir.exists()) {
SystemLogger.error(resourcesDir + " is not exist in repo (" + config.getSource() + ")", CTX);
return Collections.emptyMap();
}
return LocalFilesystemSubscription.scanLocalDirectory(resourcesDir);
} catch (Exception e) {
SystemLogger.error("Error while reading git repository", CTX, e);
throw new RuntimeException(e);
}
}
} }

View File

@ -0,0 +1,77 @@
package ru.kirillius.pf.sdn.External.API;
import org.json.JSONObject;
import org.json.JSONTokener;
import ru.kirillius.json.JSONUtility;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionProvider;
import ru.kirillius.utils.logging.SystemLogger;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Subscription provider that pulls JSON resource bundles from a Git repository cache.
*/
public class LocalFilesystemSubscription implements SubscriptionProvider {
private final static String CTX = LocalFilesystemSubscription.class.getSimpleName();
private final Context context;
/**
* Creates the provider using the shared application context.
*/
public LocalFilesystemSubscription(Context context) {
this.context = context;
}
/**
* Clones or updates the configured repository and loads resource bundles from the {@code resources} directory.
*/
@Override
public Map<String, NetworkResourceBundle> getResources(RepositoryConfig config) {
try {
var resourcesDir = new File(config.getSource());
if (!resourcesDir.exists()) {
SystemLogger.error(resourcesDir + " is not exist directory", CTX);
return Collections.emptyMap();
}
if(!config.getScript().isBlank()){
try (var shell = new ShellExecutor(ShellExecutor.Config.builder().useSSH(false).build())) {
var result = shell.executeCommand(new String[]{
config.getScript(),
config.getSource()
});
SystemLogger.message("Shell command execution result:"+result, CTX);
}
}
return scanLocalDirectory(resourcesDir);
} catch (Exception e) {
SystemLogger.error("Error while reading local FS repository", CTX, e);
throw new RuntimeException(e);
}
}
public static Map<String, NetworkResourceBundle> scanLocalDirectory(File resourcesDir) throws IOException {
var map = new HashMap<String, NetworkResourceBundle>();
for (var file : Objects.requireNonNull(resourcesDir.listFiles())) {
try (var stream = new FileInputStream(file)) {
var name = file.getName();
if (!name.endsWith(".json")) {
continue;
}
var bundle = JSONUtility.deserializeStructure(new JSONObject(new JSONTokener(stream)), NetworkResourceBundle.class);
map.put(name.substring(0, name.length() - 5), bundle);
}
}
return map;
}
}

View File

@ -14,6 +14,7 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*; import java.util.*;
/** /**
@ -29,7 +30,7 @@ public class TDNSAPI implements Closeable {
*/ */
public TDNSAPI(String server, String authToken) { public TDNSAPI(String server, String authToken) {
this.server = server; this.server = server;
httpClient = HttpClient.newBuilder().build(); httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build();
this.authToken = authToken; this.authToken = authToken;
} }

View File

@ -6,6 +6,7 @@ import ru.kirillius.json.JSONUtility;
import ru.kirillius.json.rpc.Annotations.JRPCArgument; import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCMethod; import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.pf.sdn.External.API.GitSubscription; import ru.kirillius.pf.sdn.External.API.GitSubscription;
import ru.kirillius.pf.sdn.External.API.LocalFilesystemSubscription;
import ru.kirillius.pf.sdn.InMemoryLogHandler; import ru.kirillius.pf.sdn.InMemoryLogHandler;
import ru.kirillius.pf.sdn.core.*; import ru.kirillius.pf.sdn.core.*;
import ru.kirillius.pf.sdn.web.ProtectedMethod; import ru.kirillius.pf.sdn.web.ProtectedMethod;
@ -124,6 +125,8 @@ public class System implements RPC {
public JSONArray getRepositoryTypes() { public JSONArray getRepositoryTypes() {
var array = new JSONArray(); var array = new JSONArray();
array.put(GitSubscription.class.getName()); array.put(GitSubscription.class.getName());
array.put(LocalFilesystemSubscription.class.getName());
return array; return array;
} }

View File

@ -28,4 +28,9 @@ public class RepositoryConfig {
@Getter @Getter
@JSONProperty @JSONProperty
private String source; private String source;
@Setter
@Getter
@JSONProperty(required = false)
private String script = "";
} }

View File

@ -30,7 +30,8 @@ const CLASS_NAMES = {
subscriptionRemove: 'settings-subscription-remove', subscriptionRemove: 'settings-subscription-remove',
subscriptionName: 'settings-subscription-name', subscriptionName: 'settings-subscription-name',
subscriptionType: 'settings-subscription-type', subscriptionType: 'settings-subscription-type',
subscriptionSource: 'settings-subscription-source' subscriptionSource: 'settings-subscription-source',
subscriptionScript: 'settings-subscription-script'
}; };
let currentConfig = {}; let currentConfig = {};
@ -151,6 +152,7 @@ function createSubscriptionRow(subscription = {}, index = 0) {
const name = subscription.name || ''; const name = subscription.name || '';
const type = subscription.type || ''; const type = subscription.type || '';
const source = subscription.source || ''; const source = subscription.source || '';
const script = subscription.script || '';
const uid = `${Date.now()}-${index}`; const uid = `${Date.now()}-${index}`;
return ` return `
@ -173,6 +175,10 @@ function createSubscriptionRow(subscription = {}, index = 0) {
<label for="subscription-source-${uid}">Источник</label> <label for="subscription-source-${uid}">Источник</label>
<input type="text" id="subscription-source-${uid}" class="form-control ${CLASS_NAMES.subscriptionSource}" value="${source}"> <input type="text" id="subscription-source-${uid}" class="form-control ${CLASS_NAMES.subscriptionSource}" value="${source}">
</div> </div>
<div class="form-group">
<label for="subscription-script-${uid}">Скрипт</label>
<input type="text" id="subscription-script-${uid}" class="form-control ${CLASS_NAMES.subscriptionScript}" value="${script}">
</div>
</div> </div>
`; `;
} }
@ -342,14 +348,15 @@ function collectSubscriptions() {
const name = $entry.find(`.${CLASS_NAMES.subscriptionName}`).val().trim(); const name = $entry.find(`.${CLASS_NAMES.subscriptionName}`).val().trim();
const type = $entry.find(`.${CLASS_NAMES.subscriptionType}`).val(); const type = $entry.find(`.${CLASS_NAMES.subscriptionType}`).val();
const source = $entry.find(`.${CLASS_NAMES.subscriptionSource}`).val().trim(); const source = $entry.find(`.${CLASS_NAMES.subscriptionSource}`).val().trim();
const script = $entry.find(`.${CLASS_NAMES.subscriptionScript}`).val().trim();
if (!type) { if (!type) {
hasEmptyType = true; hasEmptyType = true;
return false; return false;
} }
if (name || type || source) { if (name || type || source || script) {
subscriptions.push({ name, type, source }); subscriptions.push({ name, type, source, script });
} }
return true; return true;
}); });