API draft to manually set peer addresses

This commit is contained in:
2025-03-30 16:11:09 +02:00
parent 8b3c0a6f2c
commit 69eb96b10c
25 changed files with 2915 additions and 2399 deletions

View File

@@ -3,11 +3,9 @@ package com.usatiuk.dhfs.repository;
import com.usatiuk.dhfs.PeerId;
import com.usatiuk.dhfs.repository.peerdiscovery.PeerAddress;
import com.usatiuk.dhfs.repository.peerdiscovery.PeerDiscoveryDirectory;
import com.usatiuk.dhfs.repository.peersync.PeerInfo;
import com.usatiuk.dhfs.repository.peersync.PeerInfoService;
import com.usatiuk.dhfs.repository.peersync.api.PeerSyncApiClientDynamic;
import com.usatiuk.dhfs.repository.peertrust.PeerTrustManager;
import com.usatiuk.dhfs.repository.webapi.AvailablePeerInfo;
import com.usatiuk.objects.transaction.Transaction;
import com.usatiuk.objects.transaction.TransactionManager;
import io.quarkus.logging.Log;
@@ -70,7 +68,7 @@ public class PeerManager {
if (_heartbeatExecutor == null) return;
try {
var peers = peerInfoService.getPeersNoSelf();
var pids = peers.stream().map(PeerInfo::id).toList();
var pids = peers.stream().map(com.usatiuk.dhfs.repository.peersync.PeerInfo::id).toList();
List<PeerId> stale = _states.keySet().stream().filter(p -> !pids.contains(p)).toList();
stale.forEach(_states.keySet()::remove);
@@ -98,7 +96,7 @@ public class PeerManager {
}
}
private void handleConnectionSuccess(PeerInfo host, PeerAddress address) {
private void handleConnectionSuccess(com.usatiuk.dhfs.repository.peersync.PeerInfo host, PeerAddress address) {
boolean wasReachable = isReachable(host);
boolean shouldSync = !persistentPeerDataService.isInitialSyncDone(host.id());
@@ -120,7 +118,7 @@ public class PeerManager {
}
}
public void handleConnectionError(PeerInfo host) {
public void handleConnectionError(com.usatiuk.dhfs.repository.peersync.PeerInfo host) {
boolean wasReachable = isReachable(host);
if (wasReachable)
@@ -134,7 +132,7 @@ public class PeerManager {
}
// FIXME:
private boolean pingCheck(PeerInfo host, PeerAddress address) {
private boolean pingCheck(com.usatiuk.dhfs.repository.peersync.PeerInfo host, PeerAddress address) {
try {
return rpcClientFactory.withObjSyncClient(host.id(), address, pingTimeout, (peer, c) -> {
c.ping(PingRequest.getDefaultInstance());
@@ -150,7 +148,7 @@ public class PeerManager {
return _states.containsKey(host);
}
public boolean isReachable(PeerInfo host) {
public boolean isReachable(com.usatiuk.dhfs.repository.peersync.PeerInfo host) {
return isReachable(host.id());
}
@@ -170,7 +168,7 @@ public class PeerManager {
public HostStateSnapshot getHostStateSnapshot() {
return transactionManager.run(() -> {
var partition = peerInfoService.getPeersNoSelf().stream().map(PeerInfo::id)
var partition = peerInfoService.getPeersNoSelf().stream().map(com.usatiuk.dhfs.repository.peersync.PeerInfo::id)
.collect(Collectors.partitioningBy(this::isReachable));
return new HostStateSnapshot(partition.get(true), partition.get(false));
});
@@ -201,10 +199,9 @@ public class PeerManager {
peerTrustManager.reloadTrustManagerHosts(transactionManager.run(() -> peerInfoService.getPeers().stream().toList())); //FIXME:
}
public Collection<AvailablePeerInfo> getSeenButNotAddedHosts() {
public Collection<PeerId> getSeenButNotAddedHosts() {
return transactionManager.run(() -> {
return peerDiscoveryDirectory.getReachablePeers().stream().filter(p -> !peerInfoService.getPeerInfo(p).isPresent())
.map(p -> new AvailablePeerInfo(p.toString())).toList();
return peerDiscoveryDirectory.getReachablePeers().stream().filter(p -> !peerInfoService.getPeerInfo(p).isPresent()).toList();
});
}

View File

@@ -1,7 +1,9 @@
package com.usatiuk.dhfs.repository;
import com.usatiuk.dhfs.ShutdownChecker;
import com.usatiuk.dhfs.PeerId;
import com.usatiuk.dhfs.ShutdownChecker;
import com.usatiuk.dhfs.repository.peerdiscovery.IpPeerAddress;
import com.usatiuk.dhfs.repository.peerdiscovery.PeerAddressType;
import com.usatiuk.dhfs.repository.peersync.PeerInfoService;
import com.usatiuk.dhfs.repository.peertrust.PeerTrustManager;
import com.usatiuk.objects.transaction.Transaction;
@@ -13,6 +15,7 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.pcollections.HashTreePMap;
import org.pcollections.HashTreePSet;
import java.io.File;
@@ -23,6 +26,7 @@ import java.nio.file.StandardOpenOption;
import java.security.KeyPair;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@@ -70,7 +74,7 @@ public class PersistentPeerDataService {
_selfKeyPair = CertificateTools.generateKeyPair();
_selfCertificate = CertificateTools.generateCertificate(_selfKeyPair, _selfUuid.toString());
curTx.put(new PersistentRemoteHostsData(_selfUuid, _selfCertificate, _selfKeyPair, HashTreePSet.empty()));
curTx.put(new PersistentRemoteHostsData(_selfUuid, _selfCertificate, _selfKeyPair, HashTreePSet.empty(), HashTreePMap.empty()));
peerInfoService.putPeer(_selfUuid, _selfCertificate.getEncoded());
} catch (CertificateEncodingException e) {
throw new RuntimeException(e);
@@ -153,4 +157,39 @@ public class PersistentPeerDataService {
return data.initialSyncDone().contains(peerId);
});
}
public List<IpPeerAddress> getPersistentPeerAddresses() {
return txm.run(() -> {
var data = curTx.get(PersistentRemoteHostsData.class, PersistentRemoteHostsData.KEY).orElse(null);
if (data == null) throw new IllegalStateException("Self data not found");
return data.persistentPeerAddress().values().stream().toList();
});
}
public void addPersistentPeerAddress(PeerId peerId, IpPeerAddress address) {
txm.run(() -> {
var data = curTx.get(PersistentRemoteHostsData.class, PersistentRemoteHostsData.KEY).orElse(null);
if (data == null) throw new IllegalStateException("Self data not found");
var newData = data.persistentPeerAddress().plus(peerId, address.withType(PeerAddressType.WAN)); //TODO:
curTx.put(data.withPersistentPeerAddress(newData));
});
}
public void removePersistentPeerAddress(PeerId peerId) {
txm.run(() -> {
var data = curTx.get(PersistentRemoteHostsData.class, PersistentRemoteHostsData.KEY).orElse(null);
if (data == null) throw new IllegalStateException("Self data not found");
var newData = data.persistentPeerAddress().minus(peerId);
curTx.put(data.withPersistentPeerAddress(newData));
});
}
public IpPeerAddress getPersistentPeerAddress(PeerId peerId) {
return txm.run(() -> {
var data = curTx.get(PersistentRemoteHostsData.class, PersistentRemoteHostsData.KEY).orElse(null);
if (data == null) throw new IllegalStateException("Self data not found");
return data.persistentPeerAddress().get(peerId);
});
}
}

View File

@@ -1,8 +1,10 @@
package com.usatiuk.dhfs.repository;
import com.usatiuk.dhfs.PeerId;
import com.usatiuk.dhfs.repository.peerdiscovery.IpPeerAddress;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.dhfs.PeerId;
import org.pcollections.PMap;
import org.pcollections.PSet;
import java.io.Serializable;
@@ -12,7 +14,8 @@ import java.security.cert.X509Certificate;
public record PersistentRemoteHostsData(PeerId selfUuid,
X509Certificate selfCertificate,
KeyPair selfKeyPair,
PSet<PeerId> initialSyncDone) implements JData, Serializable {
PSet<PeerId> initialSyncDone,
PMap<PeerId, IpPeerAddress> persistentPeerAddress) implements JData, Serializable {
public static final JObjectKey KEY = JObjectKey.of("self_peer_data");
@Override
@@ -20,9 +23,12 @@ public record PersistentRemoteHostsData(PeerId selfUuid,
return KEY;
}
public PersistentRemoteHostsData withInitialSyncDone(PSet<PeerId> initialSyncDone) {
return new PersistentRemoteHostsData(selfUuid, selfCertificate, selfKeyPair, initialSyncDone);
return new PersistentRemoteHostsData(selfUuid, selfCertificate, selfKeyPair, initialSyncDone, persistentPeerAddress);
}
public PersistentRemoteHostsData withPersistentPeerAddress(PMap<PeerId, IpPeerAddress> persistentPeerAddress) {
return new PersistentRemoteHostsData(selfUuid, selfCertificate, selfKeyPair, initialSyncDone, persistentPeerAddress);
}
@Override

View File

@@ -6,4 +6,7 @@ import java.net.InetAddress;
public record IpPeerAddress(PeerId peer, PeerAddressType type,
InetAddress address, int port, int securePort) implements PeerAddress {
public IpPeerAddress withType(PeerAddressType type) {
return new IpPeerAddress(peer, type, address, port, securePort);
}
}

View File

@@ -0,0 +1,36 @@
package com.usatiuk.dhfs.repository.peerdiscovery;
import com.usatiuk.dhfs.PeerId;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;
public class PeerAddrStringHelper {
public static Optional<IpPeerAddress> parse(String addr) {
if (addr.isEmpty()) {
return Optional.empty();
}
var split = addr.split(":");
try {
return Optional.of(new IpPeerAddress(PeerId.of(split[0]), PeerAddressType.LAN, InetAddress.getByName(split[1]),
Integer.parseInt(split[2]), Integer.parseInt(split[3])));
} catch (UnknownHostException ex) {
throw new RuntimeException(ex);
}
}
public static Optional<IpPeerAddress> parseNoPeer(PeerId peerId, String addr) {
if (addr.isEmpty()) {
return Optional.empty();
}
var split = addr.split(":");
try {
return Optional.of(new IpPeerAddress(peerId, PeerAddressType.LAN, InetAddress.getByName(split[0]),
Integer.parseInt(split[1]), Integer.parseInt(split[2])));
} catch (UnknownHostException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@@ -2,7 +2,9 @@ package com.usatiuk.dhfs.repository.peerdiscovery;
import com.usatiuk.dhfs.PeerId;
public interface PeerAddress {
import java.io.Serializable;
public interface PeerAddress extends Serializable {
PeerId peer();
PeerAddressType type();

View File

@@ -0,0 +1,22 @@
package com.usatiuk.dhfs.repository.peerdiscovery;
import com.usatiuk.dhfs.repository.PersistentPeerDataService;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class PersistentStaticPeerDiscovery {
@Inject
PeerDiscoveryDirectory peerDiscoveryDirectory;
@Inject
PersistentPeerDataService persistentPeerDataService;
@Scheduled(every = "1s", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
public void discoverPeers() {
var addrs = persistentPeerDataService.getPersistentPeerAddresses();
for (var addr : addrs) {
peerDiscoveryDirectory.notifyAddr(addr);
}
}
}

View File

@@ -1,17 +1,13 @@
package com.usatiuk.dhfs.repository.peerdiscovery;
import com.usatiuk.dhfs.PeerId;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@ApplicationScoped
public class StaticPeerDiscovery {
@@ -22,18 +18,8 @@ public class StaticPeerDiscovery {
public StaticPeerDiscovery(@ConfigProperty(name = "dhfs.peerdiscovery.static-peers") Optional<String> staticPeers) {
var peers = staticPeers.orElse("");
_peers = Arrays.stream(peers.split(",")).flatMap(e ->
{
if (e.isEmpty()) {
return Stream.of();
}
var split = e.split(":");
try {
return Stream.of(new IpPeerAddress(PeerId.of(split[0]), PeerAddressType.LAN, InetAddress.getByName(split[1]),
Integer.parseInt(split[2]), Integer.parseInt(split[3])));
} catch (UnknownHostException ex) {
throw new RuntimeException(ex);
}
}).toList();
PeerAddrStringHelper.parse(e).stream()
).toList();
}
@Scheduled(every = "1s", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)

View File

@@ -1,4 +0,0 @@
package com.usatiuk.dhfs.repository.webapi;
public record AvailablePeerInfo(String uuid) {
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.dhfs.repository.webapi;
public record PeerAddressInfo(String uuid, String address) {
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.dhfs.repository.webapi;
public record PeerInfo(String uuid, String address) {
}

View File

@@ -12,8 +12,8 @@ import jakarta.ws.rs.Path;
import java.util.Collection;
import java.util.List;
@Path("/objects-manage")
public class ManagementApi {
@Path("/peers-manage")
public class PeerManagementApi {
@Inject
PeerInfoService peerInfoService;
@Inject
@@ -39,7 +39,20 @@ public class ManagementApi {
@Path("available-peers")
@GET
public Collection<AvailablePeerInfo> availablePeers() {
return peerManager.getSeenButNotAddedHosts();
public Collection<KnownPeerInfo> availablePeers() {
return peerManager.getSeenButNotAddedHosts().stream().map(p -> new KnownPeerInfo(p.toString())).toList();
}
@Path("peer-state")
@GET
public Collection<PeerInfo> peerInfos(Collection<String> peerIdStrings) {
return peerIdStrings.stream().map(PeerId::of).map(
peerId -> {
return new PeerInfo(
peerId.toString(),
peerManager.getAddress(peerId).toString()
);
}
).toList();
}
}

View File

@@ -0,0 +1,52 @@
package com.usatiuk.dhfs.repository.webapi;
import com.usatiuk.dhfs.PeerId;
import com.usatiuk.dhfs.repository.PeerManager;
import com.usatiuk.dhfs.repository.PersistentPeerDataService;
import com.usatiuk.dhfs.repository.peerdiscovery.PeerAddrStringHelper;
import com.usatiuk.dhfs.repository.peersync.PeerInfoService;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import java.util.Collection;
@Path("/peers-addr-manage")
public class PersistentPeerAddressApi {
@Inject
PeerInfoService peerInfoService;
@Inject
PeerManager peerManager;
@Inject
PersistentPeerDataService persistentPeerDataService;
@Path("{peerId}")
@PUT
public void addPeerAddress(String peerAddr, @PathParam("peerId") String peerId) {
if (peerAddr.isEmpty()) {
deletePeerAddress(peerId);
return;
}
persistentPeerDataService.addPersistentPeerAddress(PeerId.of(peerId), PeerAddrStringHelper.parseNoPeer(PeerId.of(peerId), peerAddr).orElseThrow(IllegalArgumentException::new));
}
@Path("{peerId}")
@DELETE
public void deletePeerAddress(@PathParam("peerId") String peerId) {
persistentPeerDataService.removePersistentPeerAddress(PeerId.of(peerId));
}
@Path("{peerId}")
@GET
public String getPeerAddress(@PathParam("peerId") String peerId) {
return persistentPeerDataService.getPersistentPeerAddress(PeerId.of(peerId)).toString();
}
@Path("")
@GET
public Collection<PeerAddressInfo> getPeerAddresses() {
return persistentPeerDataService.getPersistentPeerAddresses()
.stream()
.map(p -> new PeerAddressInfo(p.peer().toString(), p.address().getHostAddress() + ":" + p.port() + ":" + p.securePort()))
.toList();
}
}

View File

@@ -66,13 +66,13 @@ public class DhfsFuseIT {
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
var c2curl = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
@@ -246,7 +246,7 @@ public class DhfsFuseIT {
"curl --header \"Content-Type: application/json\" " +
" --request DELETE " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
await().atMost(45, TimeUnit.SECONDS).until(() -> 0 == container2.execInContainer("/bin/sh", "-c", "echo rewritten > /root/dhfs_default/fuse/testf1").getExitCode());
await().atMost(45, TimeUnit.SECONDS).until(() -> 0 == container2.execInContainer("/bin/sh", "-c", "echo jioadsd > /root/dhfs_default/fuse/newfile1").getExitCode());
@@ -262,7 +262,7 @@ public class DhfsFuseIT {
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);

View File

@@ -92,25 +92,25 @@ public class DhfsFusex3IT {
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
var c2curl1 = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
var c2curl3 = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c3uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
var c3curl = container3.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
waitingConsumer3.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS, 2);
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS, 2);
@@ -191,7 +191,7 @@ public class DhfsFusex3IT {
"curl --header \"Content-Type: application/json\" " +
" --request DELETE " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
Thread.sleep(10000);

View File

@@ -73,13 +73,13 @@ public class ResyncIT {
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
var c2curl = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
@@ -113,13 +113,13 @@ public class ResyncIT {
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
var c2curl = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
@@ -153,13 +153,13 @@ public class ResyncIT {
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
var c2curl = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " +
" --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers");
" http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);

4868
webui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,27 +10,31 @@
"browserslist": "> 0.5%, last 2 versions, not dead",
"dependencies": {
"jwt-decode": "^4.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.24.0",
"zod": "^3.23.8"
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.4.1",
"react-router-dom": "^7.4.1",
"zod": "^3.24.2"
},
"@parcel/resolver-default": {
"packageExports": true
},
"devDependencies": {
"@parcel/transformer-sass": "^2.12.0",
"@parcel/transformer-typescript-tsc": "^2.12.0",
"@parcel/validator-typescript": "^2.12.0",
"@types/eslint": "^8.56.10",
"@parcel/transformer-sass": "^2.14.4",
"@parcel/transformer-typescript-tsc": "^2.14.4",
"@parcel/validator-typescript": "^2.14.4",
"@types/eslint": "^9.6.1",
"@types/eslint-config-prettier": "^6.11.3",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"eslint": "^8",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.34.3",
"parcel": "^2.12.0",
"prettier": "^3.3.2",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"eslint": "^9",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-react": "^7.37.4",
"parcel": "^2.14.4",
"prettier": "^3.5.3",
"process": "^0.11.10",
"typescript": "^5.5.2"
"typescript": "^5.8.2"
}
}

View File

@@ -9,7 +9,6 @@ export interface TPeerAvailableCardProps {
export function PeerAvailableCard({ peerInfo }: TPeerAvailableCardProps) {
const fetcher = useFetcher();
return (
<div className="peerAvailableCard">
<div className={"peerInfo"}>
@@ -22,8 +21,8 @@ export function PeerAvailableCard({ peerInfo }: TPeerAvailableCardProps) {
action={"/home/peers"}
>
<button type="submit">connect</button>
<input name="intent" hidden={true} value={"add_peer"} />
<input name="uuid" hidden={true} value={peerInfo.uuid} />
<input name="intent" hidden={true} defaultValue={"add_peer"} />
<input name="uuid" hidden={true} defaultValue={peerInfo.uuid} />
</fetcher.Form>
</div>
);

View File

@@ -1,7 +1,9 @@
import { TKnownPeerInfoTo } from "./api/dto";
import "./PeerKnownCard.scss";
import { useFetcher } from "react-router-dom";
import { useFetcher, useLoaderData } from "react-router-dom";
import { LoaderToType } from "./commonPlumbing";
import { peerStateLoader } from "./PeerStatePlumbing";
export interface TPeerKnownCardProps {
peerInfo: TKnownPeerInfoTo;
@@ -9,12 +11,42 @@ export interface TPeerKnownCardProps {
export function PeerKnownCard({ peerInfo }: TPeerKnownCardProps) {
const fetcher = useFetcher();
const loaderData = useLoaderData() as LoaderToType<typeof peerStateLoader>;
const addr = loaderData.peerAddresses.find(
(item) => item.uuid === peerInfo.uuid,
);
return (
<div className="peerKnownCard">
<div className={"peerInfo"}>
<span>UUID: </span>
<span>{peerInfo.uuid}</span>
<div>
<span>UUID: </span>
<span>{peerInfo.uuid}</span>
</div>
<div>
<fetcher.Form
className="actions"
method="put"
action={"/home/peers"}
>
<input
name="intent"
hidden={true}
defaultValue={"save_addr"}
/>
<input
name="uuid"
hidden={true}
defaultValue={peerInfo.uuid}
/>
<input
name="address"
defaultValue={addr?.address || ""}
/>
<button type="submit">save</button>
</fetcher.Form>
</div>
</div>
<fetcher.Form
className="actions"
@@ -22,8 +54,12 @@ export function PeerKnownCard({ peerInfo }: TPeerKnownCardProps) {
action={"/home/peers"}
>
<button type="submit">remove</button>
<input name="intent" hidden={true} value={"remove_peer"} />
<input name="uuid" hidden={true} value={peerInfo.uuid} />
<input
name="intent"
hidden={true}
defaultValue={"remove_peer"}
/>
<input name="uuid" hidden={true} defaultValue={peerInfo.uuid} />
</fetcher.Form>
</div>
);

View File

@@ -1,7 +1,9 @@
import {
getAvailablePeers,
getKnownPeers,
getPeerAddresses,
putKnownPeer,
putPeerAddress,
removeKnownPeer,
} from "./api/PeerState";
import { ActionFunctionArgs } from "react-router-dom";
@@ -10,10 +12,15 @@ export async function peerStateLoader() {
return {
availablePeers: await getAvailablePeers(),
knownPeers: await getKnownPeers(),
peerAddresses: await getPeerAddresses(),
};
}
export type PeerStateActionType = "add_peer" | "remove_peer" | unknown;
export type PeerStateActionType =
| "add_peer"
| "remove_peer"
| "save_addr"
| unknown;
export async function peerStateAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
@@ -22,6 +29,11 @@ export async function peerStateAction({ request }: ActionFunctionArgs) {
return await putKnownPeer(formData.get("uuid") as string);
} else if (intent === "remove_peer") {
return await removeKnownPeer(formData.get("uuid") as string);
} else if (intent === "save_addr") {
return await putPeerAddress(
formData.get("uuid") as string,
formData.get("address") as string,
);
} else {
throw new Error("Malformed action: " + JSON.stringify(request));
}

View File

@@ -3,36 +3,73 @@ import {
AvailablePeerInfoToResp,
KnownPeerInfoToResp,
NoContentToResp,
PeerAddressInfoToResp,
TAvailablePeerInfoArrTo,
TAvailablePeerInfoToResp,
TKnownPeerInfoArrTo,
TKnownPeerInfoToResp,
TNoContentToResp,
TPeerAddressInfoArrTo,
TPeerAddressInfoToResp,
} from "./dto";
export async function getAvailablePeers(): Promise<TAvailablePeerInfoArrTo> {
return fetchJSON_throws<
TAvailablePeerInfoToResp,
typeof AvailablePeerInfoToResp
>("/objects-manage/available-peers", "GET", AvailablePeerInfoToResp);
>("/peers-manage/available-peers", "GET", AvailablePeerInfoToResp);
}
export async function getKnownPeers(): Promise<TKnownPeerInfoArrTo> {
return fetchJSON_throws<TKnownPeerInfoToResp, typeof KnownPeerInfoToResp>(
"/objects-manage/known-peers",
"/peers-manage/known-peers",
"GET",
KnownPeerInfoToResp,
);
}
export async function putKnownPeer(uuid: string): Promise<TNoContentToResp> {
return fetchJSON("/objects-manage/known-peers", "PUT", NoContentToResp, {
return fetchJSON("/peers-manage/known-peers", "PUT", NoContentToResp, {
uuid,
});
}
export async function removeKnownPeer(uuid: string): Promise<TNoContentToResp> {
return fetchJSON("/objects-manage/known-peers", "DELETE", NoContentToResp, {
return fetchJSON("/peers-manage/known-peers", "DELETE", NoContentToResp, {
uuid,
});
}
export async function getPeerAddresses(): Promise<TPeerAddressInfoArrTo> {
return fetchJSON_throws<
TPeerAddressInfoToResp,
typeof PeerAddressInfoToResp
>("/peers-addr-manage", "GET", PeerAddressInfoToResp);
}
export async function putPeerAddress(
uuid: string,
address: string,
): Promise<TNoContentToResp> {
return fetchJSON(
`/peers-addr-manage/${uuid}`,
"PUT",
NoContentToResp,
address,
);
}
export async function removePeerAddress(
uuid: string,
): Promise<TNoContentToResp> {
return fetchJSON(`/peers-addr-manage/${uuid}`, "DELETE", NoContentToResp);
}
export async function getPeerAddress(
uuid: string,
): Promise<TPeerAddressInfoToResp> {
return fetchJSON_throws<
TPeerAddressInfoToResp,
typeof PeerAddressInfoToResp
>(`/peers-addr-manage/${uuid}`, "GET", PeerAddressInfoToResp);
}

View File

@@ -64,6 +64,19 @@ export type TKnownPeerInfoArrTo = z.infer<typeof KnownPeerInfoArrTo>;
export const KnownPeerInfoToResp = CreateAPIResponse(KnownPeerInfoArrTo);
export type TKnownPeerInfoToResp = z.infer<typeof KnownPeerInfoToResp>;
// PeerAddressInfo
export const PeerAddressInfoTo = z.object({
uuid: z.string(),
address: z.string(),
});
export type TPeerAddressInfoTo = z.infer<typeof PeerAddressInfoTo>;
export const PeerAddressInfoArrTo = z.array(PeerAddressInfoTo);
export type TPeerAddressInfoArrTo = z.infer<typeof PeerAddressInfoArrTo>;
export const PeerAddressInfoToResp = CreateAPIResponse(PeerAddressInfoArrTo);
export type TPeerAddressInfoToResp = z.infer<typeof PeerAddressInfoToResp>;
// KnownPeerPut
export const KnownPeerPutTo = z.object({ uuid: z.string() });
export type TKnownPeerPutTo = z.infer<typeof KnownPeerPutTo>;

View File

@@ -44,14 +44,17 @@ export async function fetchJSON<T, P extends { parse: (arg: string) => T }>(
body?: string | Record<string, unknown> | File,
headers?: Record<string, string>,
): Promise<T> {
const reqBody = () =>
body instanceof File
const reqBody = () => {
if (typeof body === "string" || body instanceof String)
return body.toString();
return body instanceof File
? (() => {
const fd = new FormData();
fd.append("file", body);
return fd;
})()
: JSON.stringify(body);
};
const reqHeaders = () =>
body instanceof File

View File

@@ -6,7 +6,7 @@
],
"jsx": "react-jsx",
"target": "es2015",
"moduleResolution": "Node",
"moduleResolution": "bundler",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,