[Ready for Testing] Folder-Syncing

This commit is contained in:
Frank 2021-08-15 23:54:42 +02:00
parent 9cd0ef1f04
commit 0b9d6093a0
11 changed files with 1034 additions and 608 deletions

View file

@ -98,22 +98,22 @@ public class DataExchangeAPI extends DataExchange {
* @param fileName The name of the File
*/
public static void addAutoSyncFile(String modID, File fileName) {
getInstance().addAutoSyncFileData(modID, fileName, false, FileHash.NEED_TRANSFER);
getInstance().addAutoSyncFileData(modID, fileName, false, SyncFileHash.NEED_TRANSFER);
}
/**
* Registers a File for automatic client syncing.
* <p>
* The file is synced of the {@link FileHash} on client and server are not equal. This method will not copy the
* The file is synced of the {@link SyncFileHash} on client and server are not equal. This method will not copy the
* configs content from the client to the server.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for
* @param uniqueID A unique Identifier for the File. (see {@link SyncFileHash#uniqueID} for
* Details
* @param fileName The name of the File
*/
public static void addAutoSyncFile(String modID, String uniqueID, File fileName) {
getInstance().addAutoSyncFileData(modID, uniqueID, fileName, false, FileHash.NEED_TRANSFER);
getInstance().addAutoSyncFileData(modID, uniqueID, fileName, false, SyncFileHash.NEED_TRANSFER);
}
/**
@ -123,7 +123,7 @@ public class DataExchangeAPI extends DataExchange {
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* if the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash}
* if the File needs to be copied. Normally using the {@link SyncFileHash}
* for comparison is sufficient.
*
* @param modID The ID of the calling Mod
@ -141,11 +141,11 @@ public class DataExchangeAPI extends DataExchange {
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* if the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash}
* if the File needs to be copied. Normally using the {@link SyncFileHash}
* for comparison is sufficient.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for
* @param uniqueID A unique Identifier for the File. (see {@link SyncFileHash#uniqueID} for
* Details
* @param fileName The name of the File
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
@ -174,14 +174,14 @@ public class DataExchangeAPI extends DataExchange {
* @param modID ID of the Mod
* @return The path to the sync-folder
*/
public static File getSyncFolder(String modID) {
File fl = SYNC_FOLDER.resolve(modID.replace(".", "-")
.replace(":", "-")
.replace("\\", "-")
.replace("/", "-"))
.toFile();
public static File getModSyncFolder(String modID) {
File fl = SYNC_FOLDER.localFolder.resolve(modID.replace(".", "-")
.replace(":", "-")
.replace("\\", "-")
.replace("/", "-"))
.toFile();
if (!fl.exists()){
if (!fl.exists()) {
fl.mkdirs();
}
return fl;

View file

@ -3,8 +3,6 @@ package ru.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import org.jetbrains.annotations.NotNull;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.handler.AutoSyncID;
import ru.bclib.api.dataexchange.handler.DataExchange;
import java.io.File;
import java.io.IOException;
@ -15,182 +13,149 @@ import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Objects;
/**
* Calculates a hash based on the contents of a File.
* <p>
* A File-Hash contains the md5-sum of the File, as well as its size and byte-values from defined positions
* <p>
* You can compare instances using {@link #equals(Object)} to determine if two files are
* identical.
*/
public class FileHash extends AutoSyncID {
/**
* The md5-hash of the file
*/
@NotNull
public final byte[] md5;
/**
* The size (in bytes) of the input.
*/
public final int size;
/**
* a value that is directly calculated from defined byte positions.
*/
public final int value;
FileHash(String modID, File file, byte[] md5, int size, int value) {
this(modID, file.getName(), md5, size, value);
}
FileHash(String modID, String uniqueID, byte[] md5, int size, int value) {
super(modID, uniqueID);
Objects.nonNull(md5);
this.md5 = md5;
this.size = size;
this.value = value;
}
private static int ERR_DOES_NOT_EXIST = -10;
private static int ERR_IO_ERROR = -20;
static FileHash createForEmpty(String modID, String uniqueID, int errCode){
return new FileHash(modID, uniqueID, new byte[0], 0, errCode);
}
final static DataExchange.NeedTransferPredicate NEED_TRANSFER = (clientHash, serverHash, content)-> !clientHash.equals(serverHash);
public boolean noFile() {
return md5.length == 0;
}
@Override
public String toString() {
return super.toString()+": "+String.format("%08x", size)
+ "-"
+ String.format("%08x", value)
+ "-"
+ getMd5String();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FileHash fileHash = (FileHash) o;
return size == fileHash.size && value == fileHash.value && Arrays.equals(md5, fileHash.md5) && uniqueID.equals(fileHash.uniqueID) && modID.equals(fileHash.modID);
}
@Override
public int hashCode() {
int result = Objects.hash(size, value, uniqueID, modID);
result = 31 * result + Arrays.hashCode(md5);
return result;
}
/**
* Convert the md5-hash to a human readable string
* @return The converted String
*/
public String getMd5String(){
return toHexString(md5);
}
/**
* Serializes the Object to a buffer
* @param buf The buffer to write to
*/
public void serialize(FriendlyByteBuf buf) {
buf.writeInt(size);
buf.writeInt(value);
buf.writeByteArray(md5);
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
}
/**
*Deserialize a Buffer to a new {@link FileHash}-Object
* @param buf Thea buffer to read from
* @return The received String
*/
public static FileHash deserialize(FriendlyByteBuf buf){
final int size = buf.readInt();
final int value = buf.readInt();
final byte[] md5 = buf.readByteArray();
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
return new FileHash(modID, uniqueID, md5, size, value);
}
/**
* Converts a byte-array to a hex-string representation
* @param bytes The source array
* @return The resulting string, or an empty String if the input was {@code null}
*/
public static String toHexString(byte[] bytes) {
if (bytes==null) return "";
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* Create a new {@link FileHash}.
* <p>
* Will call {@link #create(String, File, String)} using the name of the File as {@code uniqueID}.
* @param modID ID of the calling Mod
* @param file The input file
*
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static FileHash create(String modID, File file){
return create(modID, file, file.getName());
}
/**
* Create a new {@link FileHash}.
* @param modID ID of the calling Mod
* @param file The input file
* @param uniqueID The unique ID that is used for this File (see {@link FileHash#uniqueID} for Details.
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static FileHash create(String modID, File file, String uniqueID){
if (!file.exists()) return createForEmpty(modID, uniqueID, ERR_DOES_NOT_EXIST);
final Path path = file.toPath();
int size = 0;
byte[] md5 = new byte[0];
int value = 0;
try {
byte[] data = Files.readAllBytes(path);
size = data.length;
value = size>0 ? (data[size/3] | (data [size/2]<<8) | (data [size/5]<<16)) : -1;
if (size>20) value |= data[20]<<24;
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(data);
md5 = md.digest();
return new FileHash(modID, uniqueID, md5, size, value);
} catch (IOException e) {
BCLib.LOGGER.error("Failed to read file: " + file);
return null;
} catch (NoSuchAlgorithmException e) {
BCLib.LOGGER.error("Unable to build hash for file: " + file);
}
return createForEmpty(modID, uniqueID, ERR_IO_ERROR);
}
public class FileHash {
private static int ERR_DOES_NOT_EXIST = -10;
private static int ERR_IO_ERROR = -20;
/**
* The md5-hash of the file
*/
@NotNull
public final byte[] md5;
/**
* The size (in bytes) of the input.
*/
public final int size;
/**
* a value that is directly calculated from defined byte positions.
*/
public final int value;
FileHash(byte[] md5, int size, int value) {
Objects.nonNull(md5);
this.md5 = md5;
this.size = size;
this.value = value;
}
static FileHash createForEmpty(int errCode) {
return new FileHash(new byte[0], 0, errCode);
}
public boolean noFile() {
return md5.length == 0;
}
/**
* Serializes the Object to a buffer
*
* @param buf The buffer to write to
*/
public void serialize(FriendlyByteBuf buf) {
buf.writeInt(size);
buf.writeInt(value);
buf.writeByteArray(md5);
}
/**
* Deserialize a Buffer to a new {@link SyncFileHash}-Object
*
* @param buf Thea buffer to read from
* @return The received String
*/
public static FileHash deserialize(FriendlyByteBuf buf) {
final int size = buf.readInt();
final int value = buf.readInt();
final byte[] md5 = buf.readByteArray();
return new FileHash(md5, size, value);
}
/**
* Convert the md5-hash to a human readable string
*
* @return The converted String
*/
public String getMd5String() {
return toHexString(md5);
}
/**
* Converts a byte-array to a hex-string representation
*
* @param bytes The source array
* @return The resulting string, or an empty String if the input was {@code null}
*/
public static String toHexString(byte[] bytes) {
if (bytes == null) return "";
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* Create a new {@link FileHash}.
*
* @param file The input file
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static FileHash create(File file) {
if (!file.exists()) return createForEmpty(ERR_DOES_NOT_EXIST);
final Path path = file.toPath();
int size = 0;
byte[] md5 = new byte[0];
int value = 0;
try {
byte[] data = Files.readAllBytes(path);
size = data.length;
value = size > 0 ? (data[size / 3] | (data[size / 2] << 8) | (data[size / 5] << 16)) : -1;
if (size > 20) value |= data[20] << 24;
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(data);
md5 = md.digest();
return new FileHash(md5, size, value);
}
catch (IOException e) {
BCLib.LOGGER.error("Failed to read file: " + file);
return null;
}
catch (NoSuchAlgorithmException e) {
BCLib.LOGGER.error("Unable to build hash for file: " + file);
}
return createForEmpty(ERR_IO_ERROR);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FileHash)) return false;
FileHash fileHash = (FileHash) o;
return size == fileHash.size && value == fileHash.value && Arrays.equals(md5, fileHash.md5);
}
@Override
public int hashCode() {
int result = Objects.hash(size, value);
result = 31 * result + Arrays.hashCode(md5);
return result;
}
@Override
public String toString() {
return String.format("%08x", size) + "-" + String.format("%08x", value) + "-" + getMd5String();
}
}

View file

@ -0,0 +1,108 @@
package ru.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import ru.bclib.api.dataexchange.handler.AutoSyncID;
import ru.bclib.api.dataexchange.handler.DataExchange;
import java.io.File;
import java.util.Objects;
/**
* Calculates a hash based on the contents of a File.
* <p>
* A File-Hash contains the md5-sum of the File, as well as its size and byte-values from defined positions
* <p>
* You can compare instances using {@link #equals(Object)} to determine if two files are
* identical.
*/
public class SyncFileHash extends AutoSyncID {
public final FileHash hash;
SyncFileHash(String modID, File file, byte[] md5, int size, int value) {
this(modID, file.getName(), md5, size, value);
}
SyncFileHash(String modID, String uniqueID, byte[] md5, int size, int value) {
this(modID, uniqueID, new FileHash(md5, size, value));
}
SyncFileHash(String modID, File file, FileHash hash) {
this(modID, file.getName(), hash);
}
SyncFileHash(String modID, String uniqueID, FileHash hash) {
super(modID, uniqueID);
this.hash = hash;
}
final static DataExchange.NeedTransferPredicate NEED_TRANSFER = (clientHash, serverHash, content)-> !clientHash.equals(serverHash);
@Override
public String toString() {
return super.toString()+": "+hash.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SyncFileHash)) return false;
if (!super.equals(o)) return false;
SyncFileHash that = (SyncFileHash) o;
return hash.equals(that.hash);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), hash);
}
/**
* Serializes the Object to a buffer
* @param buf The buffer to write to
*/
public void serialize(FriendlyByteBuf buf) {
hash.serialize(buf);
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
}
/**
*Deserialize a Buffer to a new {@link SyncFileHash}-Object
* @param buf Thea buffer to read from
* @return The received String
*/
public static SyncFileHash deserialize(FriendlyByteBuf buf){
final FileHash hash = FileHash.deserialize(buf);
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
return new SyncFileHash(modID, uniqueID, hash);
}
/**
* Create a new {@link SyncFileHash}.
* <p>
* Will call {@link #create(String, File, String)} using the name of the File as {@code uniqueID}.
* @param modID ID of the calling Mod
* @param file The input file
*
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static SyncFileHash create(String modID, File file){
return create(modID, file, file.getName());
}
/**
* Create a new {@link SyncFileHash}.
* @param modID ID of the calling Mod
* @param file The input file
* @param uniqueID The unique ID that is used for this File (see {@link SyncFileHash#uniqueID} for Details.
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static SyncFileHash create(String modID, File file, String uniqueID){
return new SyncFileHash(modID, uniqueID, FileHash.create(file));
}
}

View file

@ -2,7 +2,9 @@ package ru.bclib.api.dataexchange.handler;
import net.minecraft.network.FriendlyByteBuf;
import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.FileHash;
import ru.bclib.api.dataexchange.SyncFileHash;
import ru.bclib.api.dataexchange.handler.DataExchange.SyncFolderDescriptor;
import ru.bclib.api.dataexchange.handler.DataExchange.SyncFolderDescriptor.SubFile;
import ru.bclib.util.Pair;
import ru.bclib.util.Triple;
@ -12,114 +14,157 @@ import java.nio.file.Files;
import java.nio.file.Path;
class AutoFileSyncEntry extends AutoSyncID {
public final DataExchange.NeedTransferPredicate needTransfer;
public final File fileName;
public final boolean requestContent;
private FileHash hash;
AutoFileSyncEntry(String modID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) {
this(modID, fileName.getName(), fileName, requestContent, needTransfer);
}
AutoFileSyncEntry(String modID, String uniqueID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) {
super(modID, uniqueID);
this.needTransfer = needTransfer;
this.fileName = fileName;
this.requestContent = requestContent;
}
public FileHash getFileHash() {
if (hash == null)
{
hash = FileHash.create(modID, fileName, uniqueID);
}
return hash;
}
public byte[] getContent() {
if (!fileName.exists()) return new byte[0];
final Path path = fileName.toPath();
try {
return Files.readAllBytes(path);
} catch (IOException e) {
}
return new byte[0];
}
public int serializeContent(FriendlyByteBuf buf) {
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
return serializeFileContent(buf);
}
public static Triple<AutoFileSyncEntry, byte[], AutoSyncID> deserializeContent(FriendlyByteBuf buf) {
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
byte[] data = deserializeFileContent(buf);
AutoFileSyncEntry entry = AutoFileSyncEntry.findMatching(modID, uniqueID);
return new Triple<>(entry, data, new AutoSyncID(modID, uniqueID));
}
public void serialize(FriendlyByteBuf buf) {
getFileHash().serialize(buf);
buf.writeBoolean(requestContent);
if (requestContent) {
serializeFileContent(buf);
}
}
public static DataExchange.AutoSyncTriple deserializeAndMatch(FriendlyByteBuf buf) {
Pair<FileHash, byte[]> e = deserialize(buf);
AutoFileSyncEntry match = findMatching(e.first);
return new DataExchange.AutoSyncTriple(e.first, e.second, match);
}
public static Pair<FileHash, byte[]> deserialize(FriendlyByteBuf buf) {
FileHash hash = FileHash.deserialize(buf);
boolean withContent = buf.readBoolean();
byte[] data = null;
if (withContent) {
data = deserializeFileContent(buf);
}
return new Pair(hash, data);
}
private int serializeFileContent(FriendlyByteBuf buf) {
byte[] content = getContent();
buf.writeInt(content.length);
buf.writeByteArray(content);
return content.length;
}
private static byte[] deserializeFileContent(FriendlyByteBuf buf) {
byte[] data;
int size = buf.readInt();
data = buf.readByteArray(size);
return data;
}
public static AutoFileSyncEntry findMatching(FileHash hash) {
return findMatching(hash.modID, hash.uniqueID);
}
public static AutoFileSyncEntry findMatching(AutoSyncID aid) {
return findMatching(aid.modID, aid.uniqueID);
}
public static AutoFileSyncEntry findMatching(String modID, String uniqueID) {
return DataExchange
.getInstance()
.getAutoSyncFiles()
.stream()
.filter(asf -> asf.modID.equals(modID) && asf.uniqueID.equals(uniqueID))
.findFirst()
.orElse(null);
}
static class ForDirectFileRequest extends AutoFileSyncEntry {
final File relFile;
ForDirectFileRequest(String syncID, File relFile, File absFile) {
super(AutoSyncID.ForDirectFileRequest.MOD_ID, syncID, absFile, false, (a, b, c) -> false);
this.relFile = relFile;
}
@Override
public int serializeContent(FriendlyByteBuf buf) {
int res = super.serializeContent(buf);
DataHandler.writeString(buf, relFile.toString());
return res;
}
static AutoFileSyncEntry.ForDirectFileRequest finishDeserializeContent(String syncID, FriendlyByteBuf buf){
final String relFile = DataHandler.readString(buf);
SyncFolderDescriptor desc = DataExchange.getSyncFolderDescriptor(syncID);
if (desc!=null) {
return new AutoFileSyncEntry.ForDirectFileRequest(syncID, new File(relFile), desc.localFolder.resolve(relFile)
.toFile());
}
return null;
}
}
public final DataExchange.NeedTransferPredicate needTransfer;
public final File fileName;
public final boolean requestContent;
private SyncFileHash hash;
AutoFileSyncEntry(String modID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) {
this(modID, fileName.getName(), fileName, requestContent, needTransfer);
}
AutoFileSyncEntry(String modID, String uniqueID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) {
super(modID, uniqueID);
this.needTransfer = needTransfer;
this.fileName = fileName;
this.requestContent = requestContent;
}
public SyncFileHash getFileHash() {
if (hash == null) {
hash = SyncFileHash.create(modID, fileName, uniqueID);
}
return hash;
}
public byte[] getContent() {
if (!fileName.exists()) return new byte[0];
final Path path = fileName.toPath();
try {
return Files.readAllBytes(path);
}
catch (IOException e) {
}
return new byte[0];
}
public int serializeContent(FriendlyByteBuf buf) {
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
return serializeFileContent(buf);
}
public static Triple<AutoFileSyncEntry, byte[], AutoSyncID> deserializeContent(FriendlyByteBuf buf) {
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
byte[] data = deserializeFileContent(buf);
AutoFileSyncEntry entry;
if (AutoSyncID.ForDirectFileRequest.MOD_ID.equals(modID)){
entry = AutoFileSyncEntry.ForDirectFileRequest.finishDeserializeContent(uniqueID, buf);
} else {
entry = AutoFileSyncEntry.findMatching(modID, uniqueID);
}
return new Triple<>(entry, data, new AutoSyncID(modID, uniqueID));
}
public void serialize(FriendlyByteBuf buf) {
getFileHash().serialize(buf);
buf.writeBoolean(requestContent);
if (requestContent) {
serializeFileContent(buf);
}
}
public static DataExchange.AutoSyncTriple deserializeAndMatch(FriendlyByteBuf buf) {
Pair<SyncFileHash, byte[]> e = deserialize(buf);
AutoFileSyncEntry match = findMatching(e.first);
return new DataExchange.AutoSyncTriple(e.first, e.second, match);
}
public static Pair<SyncFileHash, byte[]> deserialize(FriendlyByteBuf buf) {
SyncFileHash hash = SyncFileHash.deserialize(buf);
boolean withContent = buf.readBoolean();
byte[] data = null;
if (withContent) {
data = deserializeFileContent(buf);
}
return new Pair(hash, data);
}
private int serializeFileContent(FriendlyByteBuf buf) {
byte[] content = getContent();
buf.writeInt(content.length);
buf.writeByteArray(content);
return content.length;
}
private static byte[] deserializeFileContent(FriendlyByteBuf buf) {
byte[] data;
int size = buf.readInt();
data = buf.readByteArray(size);
return data;
}
public static AutoFileSyncEntry findMatching(SyncFileHash hash) {
return findMatching(hash.modID, hash.uniqueID);
}
public static AutoFileSyncEntry findMatching(AutoSyncID aid) {
if (aid instanceof AutoSyncID.ForDirectFileRequest) {
AutoSyncID.ForDirectFileRequest freq = (AutoSyncID.ForDirectFileRequest) aid;
SyncFolderDescriptor desc = DataExchange.getSyncFolderDescriptor(freq.uniqueID);
if (desc != null) {
SubFile subFile = desc.getLocalSubFile(freq.relFile.toString());
if (subFile != null) {
final File absPath = desc.localFolder.resolve(subFile.relPath)
.toFile();
return new AutoFileSyncEntry.ForDirectFileRequest(freq.uniqueID, new File(subFile.relPath), absPath);
}
}
return null;
}
return findMatching(aid.modID, aid.uniqueID);
}
public static AutoFileSyncEntry findMatching(String modID, String uniqueID) {
return DataExchange.getInstance()
.getAutoSyncFiles()
.stream()
.filter(asf -> asf.modID.equals(modID) && asf.uniqueID.equals(uniqueID))
.findFirst()
.orElse(null);
}
}

View file

@ -1,6 +1,8 @@
package ru.bclib.api.dataexchange.handler;
import net.minecraft.network.FriendlyByteBuf;
import org.jetbrains.annotations.NotNull;
import ru.bclib.api.dataexchange.DataHandler;
import java.io.File;
import java.util.Objects;
@ -16,6 +18,27 @@ public class AutoSyncID {
this.localFile = localFile;
}
}
static class ForDirectFileRequest extends AutoSyncID {
public final static String MOD_ID = "bclib::FILE";
final File relFile;
ForDirectFileRequest(String syncID, File relFile) {
super(ForDirectFileRequest.MOD_ID, syncID);
this.relFile = relFile;
}
@Override
void serializeData(FriendlyByteBuf buf) {
super.serializeData(buf);
DataHandler.writeString(buf, relFile.toString());
}
static ForDirectFileRequest finishDeserialize(String modID, String uniqueID, FriendlyByteBuf buf){
final File fl = new File(DataHandler.readString(buf));
return new ForDirectFileRequest(uniqueID, fl);
}
}
/**
* A Unique ID for the referenced File.
* <p>
@ -56,4 +79,20 @@ public class AutoSyncID {
public int hashCode() {
return Objects.hash(uniqueID, modID);
}
void serializeData(FriendlyByteBuf buf) {
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
}
static AutoSyncID deserializeData(FriendlyByteBuf buf){
String modID = DataHandler.readString(buf);
String uID = DataHandler.readString(buf);
if (ForDirectFileRequest.MOD_ID.equals(modID)){
return ForDirectFileRequest.finishDeserialize(modID, uID, buf);
} else {
return new AutoSyncID(modID, uID);
}
}
}

View file

@ -5,230 +5,426 @@ import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.network.FriendlyByteBuf;
import org.jetbrains.annotations.NotNull;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.ConnectorClientside;
import ru.bclib.api.dataexchange.ConnectorServerside;
import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.api.dataexchange.FileHash;
import ru.bclib.api.dataexchange.SyncFileHash;
import ru.bclib.api.dataexchange.handler.AutoSyncID.ForDirectFileRequest;
import ru.bclib.config.Configs;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Stream;
abstract public class DataExchange {
public final static Path SYNC_FOLDER = FabricLoader.getInstance()
.getGameDir()
.resolve("bclib-sync")
.toAbsolutePath();
public final static String SYNC_FOLDER_ID = "BCLIB-SYNC";
@FunctionalInterface
public interface NeedTransferPredicate {
public boolean test(FileHash clientHash, FileHash serverHash, FileContentWrapper content);
}
protected final static List<BiConsumer<AutoSyncID, File>> onWriteCallbacks = new ArrayList<>(2);
final static class AutoSyncTriple {
public final FileHash serverHash;
public final byte[] serverContent;
public final AutoFileSyncEntry localMatch;
public AutoSyncTriple(FileHash serverHash, byte[] serverContent, AutoFileSyncEntry localMatch) {
this.serverHash = serverHash;
this.serverContent = serverContent;
this.localMatch = localMatch;
}
@Override
public String toString() {
return serverHash.modID+"."+serverHash.uniqueID;
}
}
private static DataExchangeAPI instance;
protected static DataExchangeAPI getInstance(){
if (instance==null){
instance = new DataExchangeAPI();
}
return instance;
}
protected ConnectorServerside server;
protected ConnectorClientside client;
protected final Set<DataHandlerDescriptor> descriptors;
private final List<AutoFileSyncEntry> autoSyncFiles = new ArrayList<>(4);
private boolean didLoadSyncFolder = false;
abstract protected ConnectorClientside clientSupplier(DataExchange api);
abstract protected ConnectorServerside serverSupplier(DataExchange api);
protected DataExchange(){
descriptors = new HashSet<>();
}
public Set<DataHandlerDescriptor> getDescriptors() { return descriptors; }
public List<AutoFileSyncEntry> getAutoSyncFiles(){
return autoSyncFiles;
}
@Environment(EnvType.CLIENT)
protected void initClientside(){
if (client!=null) return;
client = clientSupplier(this);
public final static SyncFolderDescriptor SYNC_FOLDER = new SyncFolderDescriptor("BCLIB-SYNC", FabricLoader.getInstance()
.getGameDir()
.resolve("bclib-sync")
.toAbsolutePath(), true);
final List<SyncFolderDescriptor> syncFolderDescriptions = Arrays.asList(SYNC_FOLDER);
public static class SyncFolderDescriptor {
static class SubFile {
public final String relPath;
public final FileHash hash;
SubFile(String relPath, FileHash hash) {
this.relPath = relPath;
this.hash = hash;
}
@Override
public String toString() {
return relPath;
}
public void serialize(FriendlyByteBuf buf) {
DataHandler.writeString(buf, relPath);
hash.serialize(buf);
}
public static SubFile deserialize(FriendlyByteBuf buf) {
final String relPath = DataHandler.readString(buf);
FileHash hash = FileHash.deserialize(buf);
return new SubFile(relPath, hash);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof String) return relPath.equals(o);
if (!(o instanceof SubFile)) return false;
SubFile subFile = (SubFile) o;
return relPath.equals(subFile.relPath);
}
@Override
public int hashCode() {
return relPath.hashCode();
}
}
@NotNull
public final String folderID;
public final boolean removeAdditionalFiles;
@NotNull
public final Path localFolder;
private List<SubFile> fileCache;
SyncFolderDescriptor(String folderID, Path localFolder, boolean removeAdditionalFiles) {
this.removeAdditionalFiles = removeAdditionalFiles;
this.folderID = folderID;
this.localFolder = localFolder;
fileCache = null;
}
@Override
public String toString() {
return "SyncFolderDescriptor{" + "folderID='" + folderID + '\'' + ", removeAdditionalFiles=" + removeAdditionalFiles + ", localFolder=" + localFolder + ", files=" + (fileCache == null ? "?" : fileCache.size()) + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof String) {
return folderID.equals(o);
}
if (o instanceof AutoSyncID.ForDirectFileRequest) {
return folderID.equals(((ForDirectFileRequest) o).uniqueID);
}
if (!(o instanceof SyncFolderDescriptor)) return false;
SyncFolderDescriptor that = (SyncFolderDescriptor) o;
return folderID.equals(that.folderID);
}
@Override
public int hashCode() {
return folderID.hashCode();
}
public int fileCount(){
return fileCache==null?0:fileCache.size();
}
public void invalidateCache() {
fileCache = null;
}
public void loadCache() {
if (fileCache == null) {
fileCache = new ArrayList<>(8);
fileWalker(localFolder.toFile(), p -> fileCache.add(new SubFile(localFolder.relativize(p)
.toString(), FileHash.create(p.toFile()))));
}
}
public void serialize(FriendlyByteBuf buf) {
final boolean debugHashes = Configs.CLIENT_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "debugHashes", false);
loadCache();
DataHandler.writeString(buf, folderID);
buf.writeBoolean(removeAdditionalFiles);
buf.writeInt(fileCache.size());
fileCache.forEach(fl -> {
BCLib.LOGGER.info(" - " + fl.relPath);
if (debugHashes){
BCLib.LOGGER.info(" " + fl.hash);
}
fl.serialize(buf);
});
}
public static SyncFolderDescriptor deserialize(FriendlyByteBuf buf) {
final String folderID = DataHandler.readString(buf);
final boolean remAddFiles = buf.readBoolean();
final int count = buf.readInt();
SyncFolderDescriptor localDescriptor = DataExchange.getInstance()
.getSyncFolderDescriptor(folderID);
final SyncFolderDescriptor desc;
if (localDescriptor != null) {
desc = new SyncFolderDescriptor(folderID, localDescriptor.localFolder, remAddFiles);
desc.fileCache = new ArrayList<>(count);
}
else {
BCLib.LOGGER.warning(BCLib.isClient() ? "Client" : "Server" + " does not know Sync-Folder ID '" + folderID + "'");
desc = null;
}
for (int i = 0; i < count; i++) {
SubFile relPath = SubFile.deserialize(buf);
if (desc != null) desc.fileCache.add(relPath);
}
return desc;
}
//Note: make sure loadCache was called before using this
boolean hasRelativeFile(String relFile) {
return fileCache.stream().filter(sf -> sf.equals(relFile)).findFirst().isPresent();
}
//Note: make sure loadCache was called before using this
boolean hasRelativeFile(SubFile subFile) {
return hasRelativeFile(subFile.relPath);
}
//Note: make sure loadCache was called before using this
SubFile getLocalSubFile(String relPath){
return fileCache.stream().filter(sf -> sf.relPath.equals(relPath)).findFirst().orElse(null);
}
Stream<SubFile> relativeFilesStream() {
loadCache();
return fileCache.stream();
}
}
@FunctionalInterface
public interface NeedTransferPredicate {
public boolean test(SyncFileHash clientHash, SyncFileHash serverHash, FileContentWrapper content);
}
protected final static List<BiConsumer<AutoSyncID, File>> onWriteCallbacks = new ArrayList<>(2);
final static class AutoSyncTriple {
public final SyncFileHash serverHash;
public final byte[] serverContent;
public final AutoFileSyncEntry localMatch;
public AutoSyncTriple(SyncFileHash serverHash, byte[] serverContent, AutoFileSyncEntry localMatch) {
this.serverHash = serverHash;
this.serverContent = serverContent;
this.localMatch = localMatch;
}
@Override
public String toString() {
return serverHash.modID + "." + serverHash.uniqueID;
}
}
private static DataExchangeAPI instance;
protected static DataExchangeAPI getInstance() {
if (instance == null) {
instance = new DataExchangeAPI();
}
return instance;
}
protected ConnectorServerside server;
protected ConnectorClientside client;
protected final Set<DataHandlerDescriptor> descriptors;
private final List<AutoFileSyncEntry> autoSyncFiles = new ArrayList<>(4);
private boolean didLoadSyncFolder = false;
abstract protected ConnectorClientside clientSupplier(DataExchange api);
abstract protected ConnectorServerside serverSupplier(DataExchange api);
protected DataExchange() {
descriptors = new HashSet<>();
}
public Set<DataHandlerDescriptor> getDescriptors() { return descriptors; }
public List<AutoFileSyncEntry> getAutoSyncFiles() {
return autoSyncFiles;
}
@Environment(EnvType.CLIENT)
protected void initClientside() {
if (client != null) return;
client = clientSupplier(this);
/*ClientLoginConnectionEvents.INIT.register((a, b) ->{
System.out.println("INIT");
});
ClientLoginConnectionEvents.QUERY_START.register((a, b) ->{
System.out.println("INIT");
});*/
ClientPlayConnectionEvents.INIT.register(client::onPlayInit);
ClientPlayConnectionEvents.JOIN.register(client::onPlayReady);
ClientPlayConnectionEvents.DISCONNECT.register(client::onPlayDisconnect);
}
protected void initServerSide(){
if (server!=null) return;
server = serverSupplier(this);
ServerPlayConnectionEvents.INIT.register(server::onPlayInit);
ServerPlayConnectionEvents.JOIN.register(server::onPlayReady);
ServerPlayConnectionEvents.DISCONNECT.register(server::onPlayDisconnect);
}
/**
* Initializes all datastructures that need to exist in the client component.
* <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/
@Environment(EnvType.CLIENT)
public static void prepareClientside(){
DataExchange api = DataExchange.getInstance();
api.initClientside();
}
/**
* Initializes all datastructures that need to exist in the server component.
* <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/
public static void prepareServerside(){
DataExchange api = DataExchange.getInstance();
api.initServerSide();
}
/**
* Automatically called before the player enters the world.
* <p>
* This is automatically called by BCLib. It will send all {@link DataHandler}-Objects that have {@link DataHandlerDescriptor#sendBeforeEnter} set to*
* {@code true},
*/
@Environment(EnvType.CLIENT)
public static void sendOnEnter(){
getInstance().descriptors.forEach((desc)-> {
if (desc.sendBeforeEnter){
DataHandler h = desc.JOIN_INSTANCE.get();
if (!h.getOriginatesOnServer()) {
getInstance().client.sendToServer(h);
}
}
});
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
* @param fileName The name of the File
* @param requestContent When {@code true} the content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* If the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash}
* for comparison is sufficient.
*/
protected void addAutoSyncFileData(String modID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer){
autoSyncFiles.add(new AutoFileSyncEntry(modID, fileName, requestContent, needTransfer));
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for
* Details
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
* @param fileName The name of the File
* @param requestContent When {@code true} the content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* If the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash}
* for comparison is sufficient.
*/
protected void addAutoSyncFileData(String modID, String uniqueID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer){
autoSyncFiles.add(new AutoFileSyncEntry(modID, uniqueID, fileName, requestContent, needTransfer));
}
/**
* Called when {@code SendFiles} received a File on the Client and wrote it to the FileSystem.
* <p>
* This is the place where reload Code should go.
* @param aid The ID of the received File
* @param file The location of the FIle on the client
*/
static void didReceiveFile(AutoSyncID aid, File file){
onWriteCallbacks.forEach(fkt -> fkt.accept(aid, file));
}
private List<String> syncFolderContent;
protected List<String> getSyncFolderContent(){
if (syncFolderContent==null){
return new ArrayList<>(0);
}
return syncFolderContent;
}
//we call this from HelloServer to prepare transfer
protected void loadSyncFolder() {
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offserSyncFolder", true))
{
final File syncPath = SYNC_FOLDER.toFile();
if (!syncPath.exists()) {
syncPath.mkdirs();
}
if (syncFolderContent == null) {
syncFolderContent = new ArrayList<>(8);
addFilesForSyncFolder(syncPath);
}
}
}
private void addFilesForSyncFolder(File path){
for (final File f : path.listFiles()) {
if (f.isDirectory()) {
addFilesForSyncFolder(f);
} else if (f.isFile()) {
if (!f.getName().startsWith(".")) {
Path p = f.toPath();
p = SYNC_FOLDER.relativize(p);
syncFolderContent.add(p.toString());
}
}
}
}
ClientPlayConnectionEvents.INIT.register(client::onPlayInit);
ClientPlayConnectionEvents.JOIN.register(client::onPlayReady);
ClientPlayConnectionEvents.DISCONNECT.register(client::onPlayDisconnect);
}
protected void initServerSide() {
if (server != null) return;
server = serverSupplier(this);
ServerPlayConnectionEvents.INIT.register(server::onPlayInit);
ServerPlayConnectionEvents.JOIN.register(server::onPlayReady);
ServerPlayConnectionEvents.DISCONNECT.register(server::onPlayDisconnect);
}
/**
* Initializes all datastructures that need to exist in the client component.
* <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/
@Environment(EnvType.CLIENT)
public static void prepareClientside() {
DataExchange api = DataExchange.getInstance();
api.initClientside();
}
/**
* Initializes all datastructures that need to exist in the server component.
* <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/
public static void prepareServerside() {
DataExchange api = DataExchange.getInstance();
api.initServerSide();
}
/**
* Automatically called before the player enters the world.
* <p>
* This is automatically called by BCLib. It will send all {@link DataHandler}-Objects that have {@link DataHandlerDescriptor#sendBeforeEnter} set to*
* {@code true},
*/
@Environment(EnvType.CLIENT)
public static void sendOnEnter() {
getInstance().descriptors.forEach((desc) -> {
if (desc.sendBeforeEnter) {
DataHandler h = desc.JOIN_INSTANCE.get();
if (!h.getOriginatesOnServer()) {
getInstance().client.sendToServer(h);
}
}
});
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
* @param fileName The name of the File
* @param requestContent When {@code true} the content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* If the File needs to be copied. Normally using the {@link SyncFileHash}
* for comparison is sufficient.
*/
protected void addAutoSyncFileData(String modID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer) {
autoSyncFiles.add(new AutoFileSyncEntry(modID, fileName, requestContent, needTransfer));
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link SyncFileHash#uniqueID} for
* Details
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
* @param fileName The name of the File
* @param requestContent When {@code true} the content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* If the File needs to be copied. Normally using the {@link SyncFileHash}
* for comparison is sufficient.
*/
protected void addAutoSyncFileData(String modID, String uniqueID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer) {
autoSyncFiles.add(new AutoFileSyncEntry(modID, uniqueID, fileName, requestContent, needTransfer));
}
/**
* Called when {@code SendFiles} received a File on the Client and wrote it to the FileSystem.
* <p>
* This is the place where reload Code should go.
*
* @param aid The ID of the received File
* @param file The location of the FIle on the client
*/
static void didReceiveFile(AutoSyncID aid, File file) {
onWriteCallbacks.forEach(fkt -> fkt.accept(aid, file));
}
private List<String> syncFolderContent;
protected List<String> getSyncFolderContent() {
if (syncFolderContent == null) {
return new ArrayList<>(0);
}
return syncFolderContent;
}
//we call this from HelloServer to prepare transfer
protected void loadSyncFolder() {
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offersSyncFolders", true)) {
syncFolderDescriptions.forEach(desc -> desc.loadCache());
}
}
protected static SyncFolderDescriptor getSyncFolderDescriptor(String folderID) {
return ((DataExchange) getInstance()).syncFolderDescriptions.stream()
.filter(d -> d.equals(folderID))
.findFirst()
.orElse(null);
}
protected static Path localBasePathForFolderID(String folderID) {
final SyncFolderDescriptor desc = getSyncFolderDescriptor(folderID);
if (desc != null) {
return desc.localFolder;
}
else {
BCLib.LOGGER.warning("Unknown Sync-Folder ID '" + folderID + "'");
return null;
}
}
protected void registerSyncFolder(String folderID, Path localBaseFolder, boolean removeAdditionalFiles) {
final SyncFolderDescriptor desc = new SyncFolderDescriptor(folderID, localBaseFolder, removeAdditionalFiles);
if (this.syncFolderDescriptions.contains(desc)) {
BCLib.LOGGER.warning("Tried to override Folder Sync '" + folderID + "' again.");
}
else {
this.syncFolderDescriptions.add(desc);
}
}
/**
* A simple directory walker that ignores dot-files
*
* @param path The path where you want to start
* @param pathConsumer The consumer called for each valid file. The consumer will get an absolute {@link Path}-Object
* for each visited file
*/
public static void fileWalker(File path, Consumer<Path> pathConsumer) {
if (!path.exists()) return;
for (final File f : path.listFiles()) {
if (f.getName()
.startsWith(".")) continue;
if (f.isDirectory()) {
fileWalker(f, pathConsumer);
}
else if (f.isFile()) {
pathConsumer.accept(f.toPath());
}
}
}
}

View file

@ -15,11 +15,14 @@ import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.api.dataexchange.handler.AutoSyncID.WithContentOverride;
import ru.bclib.api.dataexchange.handler.DataExchange.SyncFolderDescriptor;
import ru.bclib.api.dataexchange.handler.DataExchange.SyncFolderDescriptor.SubFile;
import ru.bclib.api.datafixer.DataFixerAPI;
import ru.bclib.config.Configs;
import ru.bclib.gui.screens.SyncFilesScreen;
import ru.bclib.gui.screens.WarnBCLibVersionMismatch;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -41,28 +44,31 @@ public class HelloClient extends DataHandler {
super(DESCRIPTOR.IDENTIFIER, true);
}
public static String getModVersion(String modID){
Optional<ModContainer> optional = FabricLoader.getInstance().getModContainer(modID);
public static String getModVersion(String modID) {
Optional<ModContainer> optional = FabricLoader.getInstance()
.getModContainer(modID);
if (optional.isPresent()) {
ModContainer modContainer = optional.get();
return modContainer.getMetadata().getVersion().toString();
return modContainer.getMetadata()
.getVersion()
.toString();
}
return "0.0.0";
}
static String getBCLibVersion(){
static String getBCLibVersion() {
return getModVersion(BCLib.MOD_ID);
}
@Override
protected void serializeData(FriendlyByteBuf buf) {
final String vbclib = getBCLibVersion();
BCLib.LOGGER.info("Sending Hello to Client. (server="+vbclib+")");
BCLib.LOGGER.info("Sending Hello to Client. (server=" + vbclib + ")");
final List<String> mods = DataExchangeAPI.registeredMods();
//write BCLibVersion (=protocol version)
buf.writeInt(DataFixerAPI.getModVersion(vbclib));
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offerMods", true)) {
//write Plugin Versions
buf.writeInt(mods.size());
@ -72,128 +78,162 @@ public class HelloClient extends DataHandler {
buf.writeInt(DataFixerAPI.getModVersion(ver));
BCLib.LOGGER.info(" - Listing Mod " + modID + " v" + ver);
}
} else {
}
else {
BCLib.LOGGER.info("Server will not list Mods.");
buf.writeInt(0);
}
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offerFiles", true)) {
//do only include files that exist on the server
final List<AutoFileSyncEntry> existingAutoSyncFiles = DataExchange
.getInstance()
.getAutoSyncFiles()
.stream()
.filter(e -> e.fileName.exists())
.collect(Collectors.toList());
final List<AutoFileSyncEntry> existingAutoSyncFiles = DataExchange.getInstance()
.getAutoSyncFiles()
.stream()
.filter(e -> e.fileName.exists())
.collect(Collectors.toList());
//send config Data
buf.writeInt(existingAutoSyncFiles.size());
for (AutoFileSyncEntry entry : existingAutoSyncFiles) {
entry.serialize(buf);
BCLib.LOGGER.info(" - Offering File " + entry);
}
} else {
}
else {
BCLib.LOGGER.info("Server will not offer Files.");
buf.writeInt(0);
}
//for the moment this is only hardcoded for the sync-folder offered by BCLIB, but it can be extended in future
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offserSyncFolder", true)) {
buf.writeInt(1); //currently we do only sync a single folder
writeString(buf, DataExchange.SYNC_FOLDER_ID); //the UID of the Folder
final List<String> fileNames = DataExchange.getInstance().getSyncFolderContent();
buf.writeInt(fileNames.size());
fileNames.forEach(fl -> writeString(buf, fl));
} else {
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offersSyncFolders", true)) {
buf.writeInt(((DataExchange) DataExchange.getInstance()).syncFolderDescriptions.size());
((DataExchange) DataExchange.getInstance()).syncFolderDescriptions.forEach(desc -> {
BCLib.LOGGER.info(" - Offering Folder " + desc.localFolder + " (allowDelete="+desc.removeAdditionalFiles+")");
desc.serialize(buf);
});
}
else {
BCLib.LOGGER.info("Server will not offer Sync Folders.");
buf.writeInt(0);
}
Configs.MAIN_CONFIG.saveChanges();
}
String bclibVersion ="0.0.0";
String bclibVersion = "0.0.0";
Map<String, String> modVersion = new HashMap<>();
List<DataExchange.AutoSyncTriple> autoSyncedFiles = null;
List<SyncFolderDescriptor> autoSynFolders = null;
@Override
protected void deserializeFromIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean fromClient) {
//read BCLibVersion (=protocol version)
bclibVersion = DataFixerAPI.getModVersion(buf.readInt());
//read Plugin Versions
modVersion = new HashMap<>();
int count = buf.readInt();
for (int i=0; i< count; i++) {
for (int i = 0; i < count; i++) {
String id = readString(buf);
String version = DataFixerAPI.getModVersion(buf.readInt());
modVersion.put(id, version);
}
//read config Data
count = buf.readInt();
autoSyncedFiles = new ArrayList<>(count);
for (int i=0; i< count; i++) {
for (int i = 0; i < count; i++) {
//System.out.println("Deserializing ");
DataExchange.AutoSyncTriple t = AutoFileSyncEntry.deserializeAndMatch(buf);
autoSyncedFiles.add(t);
//System.out.println(t.first);
}
//since this version we also send the sync folders
autoSynFolders = new ArrayList<>(1);
//since v0.4.1 we also send the sync folders
if (DataFixerAPI.isLargerOrEqualVersion(bclibVersion, "0.4.1")) {
final int folderCount = buf.readInt();
for (int i=0; i<folderCount; i++){
final String folderID = readString(buf);
final int entries = buf.readInt();
List<String> files = new ArrayList<>(entries);
for (int j=0; j<entries; j++){
files.add(readString(buf));
}
if (folderID.equals(DataExchange.SYNC_FOLDER_ID)) {
//TODO: implement the syncing here
} else {
BCLib.LOGGER.warning("Unknown Sync-Folder '"+folderID+"'");
}
for (int i = 0; i < folderCount; i++) {
SyncFolderDescriptor desc = SyncFolderDescriptor.deserialize(buf);
autoSynFolders.add(desc);
}
}
}
@Override
protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient) {
final boolean debugHashes = Configs.CLIENT_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "debugHashes", false);
final String localBclibVersion = getBCLibVersion();
BCLib.LOGGER.info("Received Hello from Server. (client="+localBclibVersion+", server="+bclibVersion+")");
// if (DataFixerAPI.getModVersion(localBclibVersion) != DataFixerAPI.getModVersion(bclibVersion)){
// showBCLibError(client);
// return;
// }
final List<AutoSyncID> filesToRequest = new ArrayList<>(2);
for (Entry<String, String> e : modVersion.entrySet()){
String ver = getModVersion(e.getKey());
BCLib.LOGGER.info(" - " + e.getKey() + " (client="+ver+", server="+ver+")");
private void processAutoSyncFolder(final List<AutoSyncID> filesToRequest, final List<AutoSyncID.ForDirectFileRequest> filesToRemove) {
if (!Configs.CLIENT_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "syncFolders", true)) {
return;
}
if (autoSyncedFiles.size()>0) {
if (autoSynFolders.size() > 0) {
BCLib.LOGGER.info("Folders offered by Server:");
}
autoSynFolders.forEach(desc -> {
//desc contains the fileCache sent from the server, load the local version to get hold of the actual file cache on the client
SyncFolderDescriptor localDescriptor = DataExchange.getSyncFolderDescriptor(desc.folderID);
if (localDescriptor != null) {
BCLib.LOGGER.info(" - " + desc.folderID + " (" + desc.localFolder + ", allowRemove=" + desc.removeAdditionalFiles + ")");
localDescriptor.invalidateCache();
if (desc.removeAdditionalFiles) {
List<AutoSyncID.ForDirectFileRequest> additionalFiles = localDescriptor.relativeFilesStream()
.filter(subFile -> !desc.hasRelativeFile(subFile))
.map(subFile -> new AutoSyncID.ForDirectFileRequest(desc.folderID, desc.localFolder.resolve(subFile.relPath).toFile()))
.collect(Collectors.toList());
additionalFiles.forEach(aid -> BCLib.LOGGER.info(" * Removing " + aid.relFile));
filesToRemove.addAll(additionalFiles);
}
desc.relativeFilesStream().forEach(subFile -> {
SubFile localSubFile = localDescriptor.getLocalSubFile(subFile.relPath);
if (localSubFile != null){
//the file exists locally, check if the hashes match
if (!localSubFile.hash.equals(subFile.hash)){
BCLib.LOGGER.info(" * Changed " + subFile.relPath);
filesToRequest.add(new AutoSyncID.ForDirectFileRequest(desc.folderID, new File(subFile.relPath)));
}
} else {
//the file is missing locally
BCLib.LOGGER.info(" * Missing " + subFile.relPath);
filesToRequest.add(new AutoSyncID.ForDirectFileRequest(desc.folderID, new File(subFile.relPath)));
}
});
//free some memory
localDescriptor.invalidateCache();
}
else {
BCLib.LOGGER.info(" - " + desc.folderID + " (Failed to find)");
}
});
}
private void processSingleFileSync(final List<AutoSyncID> filesToRequest) {
final boolean debugHashes = Configs.CLIENT_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "debugHashes", false);
if (autoSyncedFiles.size() > 0) {
BCLib.LOGGER.info("Files offered by Server:");
}
//Handle single sync files
//Single files need to be registered for sync on both client and server
//There are no restrictions to the target folder, but the client decides the final
//location.
for (DataExchange.AutoSyncTriple e : autoSyncedFiles) {
String actionString = "";
FileContentWrapper contentWrapper = new FileContentWrapper(e.serverContent);
if (e.localMatch == null) {
actionString = "(new, prepare update)";
filesToRequest.add(new AutoSyncID(e.serverHash.modID, e.serverHash.uniqueID));
} else if (e.localMatch.needTransfer.test(e.localMatch.getFileHash(), e.serverHash, contentWrapper)) {
}
else if (e.localMatch.needTransfer.test(e.localMatch.getFileHash(), e.serverHash, contentWrapper)) {
actionString = "(prepare update)";
//we did not yet receive the new content
if (contentWrapper.getRawContent() == null) {
filesToRequest.add(new AutoSyncID(e.serverHash.modID, e.serverHash.uniqueID));
} else {
}
else {
filesToRequest.add(new AutoSyncID.WithContentOverride(e.serverHash.modID, e.serverHash.uniqueID, contentWrapper, e.localMatch.fileName));
}
}
@ -205,20 +245,48 @@ public class HelloClient extends DataHandler {
BCLib.LOGGER.info(" * local Content " + (contentWrapper.getRawContent() == null));
}
}
if (filesToRequest.size()>0 && SendFiles.acceptFiles()) {
showDownloadConfigs(client, filesToRequest);
}
@Override
protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient) {
final String localBclibVersion = getBCLibVersion();
BCLib.LOGGER.info("Received Hello from Server. (client=" + localBclibVersion + ", server=" + bclibVersion + ")");
// if (DataFixerAPI.getModVersion(localBclibVersion) != DataFixerAPI.getModVersion(bclibVersion)){
// showBCLibError(client);
// return;
// }
final List<AutoSyncID> filesToRequest = new ArrayList<>(2);
final List<AutoSyncID.ForDirectFileRequest> filesToRemove = new ArrayList<>(2);
for (Entry<String, String> e : modVersion.entrySet()) {
String ver = getModVersion(e.getKey());
BCLib.LOGGER.info(" - " + e.getKey() + " (client=" + ver + ", server=" + ver + ")");
}
processSingleFileSync(filesToRequest);
processAutoSyncFolder(filesToRequest, filesToRemove);
//Handle folder sync
//Both client and server need to know about the folder you want to sync
//Files can only get placed within that folder
if ((filesToRequest.size() > 0 || filesToRemove.size() > 0) && SendFiles.acceptFiles()) {
showDownloadConfigs(client, filesToRequest, filesToRemove);
return;
}
}
@Environment(EnvType.CLIENT)
protected void showBCLibError(Minecraft client){
protected void showBCLibError(Minecraft client) {
BCLib.LOGGER.error("BCLib differs on client and server.");
client.setScreen(new WarnBCLibVersionMismatch((download) -> {
Minecraft.getInstance().setScreen((Screen)null);
if (download){
requestBCLibDownload((hadErrors)->{
Minecraft.getInstance()
.setScreen((Screen) null);
if (download) {
requestBCLibDownload((hadErrors) -> {
client.stop();
});
}
@ -226,38 +294,45 @@ public class HelloClient extends DataHandler {
}
@Environment(EnvType.CLIENT)
protected void showDownloadConfigs(Minecraft client, List<AutoSyncID> files){
protected void showDownloadConfigs(Minecraft client, List<AutoSyncID> files, final List<AutoSyncID.ForDirectFileRequest> filesToRemove) {
client.setScreen(new SyncFilesScreen((download) -> {
Minecraft.getInstance().setScreen((Screen)null);
if (download){
Minecraft.getInstance()
.setScreen((Screen) null);
if (download) {
BCLib.LOGGER.info("Updating local Files:");
List<AutoSyncID.WithContentOverride> localChanges = new ArrayList<>(files.toArray().length);
List<AutoSyncID> requestFiles = new ArrayList<>(files.toArray().length);
files.forEach(aid -> {
if (aid instanceof WithContentOverride) {
final WithContentOverride aidc = (WithContentOverride)aid;
final WithContentOverride aidc = (WithContentOverride) aid;
BCLib.LOGGER.info(" - " + aid + " (updating Content)");
SendFiles.writeSyncedFile(aid, aidc.contentWrapper.getRawContent(), aidc.localFile);
} else {
}
else {
requestFiles.add(aid);
BCLib.LOGGER.info(" - " + aid + " (requesting)");
}
});
filesToRemove.forEach(aid -> {
BCLib.LOGGER.info(" - " + aid.relFile + " (removing)");
aid.relFile.delete();
});
requestFileDownloads(requestFiles);
}
}));
}
private void requestBCLibDownload(Consumer<Boolean> whenFinished){
private void requestBCLibDownload(Consumer<Boolean> whenFinished) {
BCLib.LOGGER.warning("Starting download of BCLib");
whenFinished.accept(true);
}
private void requestFileDownloads(List<AutoSyncID> files){
private void requestFileDownloads(List<AutoSyncID> files) {
BCLib.LOGGER.info("Starting download of Files:" + files.size());
DataExchangeAPI.send(new RequestFiles(files));
}

View file

@ -36,8 +36,7 @@ public class RequestFiles extends DataHandler {
buf.writeInt(files.size());
for (AutoSyncID a : files){
writeString(buf, a.modID);
writeString(buf, a.uniqueID);
a.serializeData(buf);
}
}
@ -50,9 +49,7 @@ public class RequestFiles extends DataHandler {
BCLib.LOGGER.info("Client requested " + size + " Files:");
for (int i=0; i<size; i++){
String modID = readString(buf);
String uID = readString(buf);
AutoSyncID asid = new AutoSyncID(modID, uID);
AutoSyncID asid = AutoSyncID.deserializeData(buf);
files.add(asid);
BCLib.LOGGER.info(" - " + asid);
}

View file

@ -108,6 +108,11 @@ public class SendFiles extends DataHandler {
Path path = fileName.toPath();
BCLib.LOGGER.info(" - Writing " + path + " (" + data.length + " Bytes)");
try {
final File parentFile = path.getParent()
.toFile();
if (!parentFile.exists()){
parentFile.mkdirs();
}
Files.write(path, data);
DataExchange.didReceiveFile(e, fileName);
} catch (IOException ioException) {

View file

@ -3,7 +3,7 @@ package ru.bclib.config;
import org.jetbrains.annotations.Nullable;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.dataexchange.FileHash;
import ru.bclib.api.dataexchange.SyncFileHash;
import ru.bclib.api.dataexchange.handler.AutoSyncID;
import ru.bclib.api.dataexchange.handler.FileContentWrapper;
import ru.bclib.config.ConfigKeeper.BooleanEntry;
@ -50,7 +50,7 @@ public abstract class Config {
}
}
private boolean compareForSync(FileHash fileHash, FileHash fileHash1, FileContentWrapper content) {
private boolean compareForSync(SyncFileHash syncFileHash, SyncFileHash syncFileHash1, FileContentWrapper content) {
return keeper.compareAndUpdateForSync(content);
}

View file

@ -84,7 +84,7 @@ public final class ConfigKeeper {
changed = true;
me.add(myKey.first + myKey.second, otherValue);
}
else if (otherValue.isJsonPrimitive()) {
else if (otherValue.isJsonPrimitive() || otherValue.isJsonArray() || otherValue.isJsonNull()) {
if (!otherValue.equals(myValue)) {
changed = true;
me.add(myKey.first + myKey.second, otherValue);
@ -93,17 +93,13 @@ public final class ConfigKeeper {
else if (otherValue.isJsonObject()) {
changed |= compareAndUpdateForSync(myValue.getAsJsonObject(), otherValue.getAsJsonObject());
}
else if (otherValue.isJsonArray()) {
if (!otherValue.equals(myValue)) {
changed = true;
me.add(myKey.first + myKey.second, otherValue);
}
}
}
else { //no entry, just copy the value from other
changed = true;
me.add(otherKey.first + otherKey.second, otherValue);
if (!otherValue.isJsonNull()) {
changed = true;
temp = find(me, otherKey);
me.add(otherKey.first + otherKey.second, otherValue);
}
}
}