[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 * @param fileName The name of the File
*/ */
public static void addAutoSyncFile(String modID, File fileName) { 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. * Registers a File for automatic client syncing.
* <p> * <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. * configs content from the client to the server.
* *
* @param modID The ID of the calling Mod * @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 * Details
* @param fileName The name of the File * @param fileName The name of the File
*/ */
public static void addAutoSyncFile(String modID, String uniqueID, File fileName) { 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. * entire file from the client to the server.
* <p> * <p>
* You should only use this option, if you need to compare parts of the file in order to decide * 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. * for comparison is sufficient.
* *
* @param modID The ID of the calling Mod * @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. * entire file from the client to the server.
* <p> * <p>
* You should only use this option, if you need to compare parts of the file in order to decide * 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. * for comparison is sufficient.
* *
* @param modID The ID of the calling Mod * @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 * Details
* @param fileName The name of the File * @param fileName The name of the File
* @param needTransfer If the predicate returns true, the file needs to get copied to the server. * @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 * @param modID ID of the Mod
* @return The path to the sync-folder * @return The path to the sync-folder
*/ */
public static File getSyncFolder(String modID) { public static File getModSyncFolder(String modID) {
File fl = SYNC_FOLDER.resolve(modID.replace(".", "-") File fl = SYNC_FOLDER.localFolder.resolve(modID.replace(".", "-")
.replace(":", "-") .replace(":", "-")
.replace("\\", "-") .replace("\\", "-")
.replace("/", "-")) .replace("/", "-"))
.toFile(); .toFile();
if (!fl.exists()){ if (!fl.exists()) {
fl.mkdirs(); fl.mkdirs();
} }
return fl; return fl;

View file

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

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 net.minecraft.network.FriendlyByteBuf;
import ru.bclib.api.dataexchange.DataHandler; 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.Pair;
import ru.bclib.util.Triple; import ru.bclib.util.Triple;
@ -12,114 +14,157 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
class AutoFileSyncEntry extends AutoSyncID { class AutoFileSyncEntry extends AutoSyncID {
public final DataExchange.NeedTransferPredicate needTransfer; static class ForDirectFileRequest extends AutoFileSyncEntry {
public final File fileName; final File relFile;
public final boolean requestContent;
private FileHash hash; ForDirectFileRequest(String syncID, File relFile, File absFile) {
super(AutoSyncID.ForDirectFileRequest.MOD_ID, syncID, absFile, false, (a, b, c) -> false);
AutoFileSyncEntry(String modID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) { this.relFile = relFile;
this(modID, fileName.getName(), fileName, requestContent, needTransfer); }
}
@Override
AutoFileSyncEntry(String modID, String uniqueID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) { public int serializeContent(FriendlyByteBuf buf) {
super(modID, uniqueID); int res = super.serializeContent(buf);
this.needTransfer = needTransfer; DataHandler.writeString(buf, relFile.toString());
this.fileName = fileName;
this.requestContent = requestContent; return res;
} }
public FileHash getFileHash() { static AutoFileSyncEntry.ForDirectFileRequest finishDeserializeContent(String syncID, FriendlyByteBuf buf){
if (hash == null) final String relFile = DataHandler.readString(buf);
{ SyncFolderDescriptor desc = DataExchange.getSyncFolderDescriptor(syncID);
hash = FileHash.create(modID, fileName, uniqueID); if (desc!=null) {
} return new AutoFileSyncEntry.ForDirectFileRequest(syncID, new File(relFile), desc.localFolder.resolve(relFile)
return hash; .toFile());
} }
return null;
public byte[] getContent() { }
if (!fileName.exists()) return new byte[0]; }
final Path path = fileName.toPath(); public final DataExchange.NeedTransferPredicate needTransfer;
public final File fileName;
try { public final boolean requestContent;
return Files.readAllBytes(path); private SyncFileHash hash;
} catch (IOException e) {
AutoFileSyncEntry(String modID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) {
} this(modID, fileName.getName(), fileName, requestContent, needTransfer);
return new byte[0]; }
}
AutoFileSyncEntry(String modID, String uniqueID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) {
public int serializeContent(FriendlyByteBuf buf) { super(modID, uniqueID);
DataHandler.writeString(buf, modID); this.needTransfer = needTransfer;
DataHandler.writeString(buf, uniqueID); this.fileName = fileName;
return serializeFileContent(buf); this.requestContent = requestContent;
} }
public static Triple<AutoFileSyncEntry, byte[], AutoSyncID> deserializeContent(FriendlyByteBuf buf) { public SyncFileHash getFileHash() {
final String modID = DataHandler.readString(buf); if (hash == null) {
final String uniqueID = DataHandler.readString(buf); hash = SyncFileHash.create(modID, fileName, uniqueID);
byte[] data = deserializeFileContent(buf); }
return hash;
AutoFileSyncEntry entry = AutoFileSyncEntry.findMatching(modID, uniqueID); }
return new Triple<>(entry, data, new AutoSyncID(modID, uniqueID));
} public byte[] getContent() {
if (!fileName.exists()) return new byte[0];
final Path path = fileName.toPath();
public void serialize(FriendlyByteBuf buf) {
getFileHash().serialize(buf); try {
buf.writeBoolean(requestContent); return Files.readAllBytes(path);
}
if (requestContent) { catch (IOException e) {
serializeFileContent(buf);
} }
} return new byte[0];
}
public static DataExchange.AutoSyncTriple deserializeAndMatch(FriendlyByteBuf buf) {
Pair<FileHash, byte[]> e = deserialize(buf); public int serializeContent(FriendlyByteBuf buf) {
AutoFileSyncEntry match = findMatching(e.first); DataHandler.writeString(buf, modID);
return new DataExchange.AutoSyncTriple(e.first, e.second, match); DataHandler.writeString(buf, uniqueID);
} return serializeFileContent(buf);
}
public static Pair<FileHash, byte[]> deserialize(FriendlyByteBuf buf) {
FileHash hash = FileHash.deserialize(buf); public static Triple<AutoFileSyncEntry, byte[], AutoSyncID> deserializeContent(FriendlyByteBuf buf) {
boolean withContent = buf.readBoolean(); final String modID = DataHandler.readString(buf);
byte[] data = null; final String uniqueID = DataHandler.readString(buf);
if (withContent) { byte[] data = deserializeFileContent(buf);
data = deserializeFileContent(buf);
} AutoFileSyncEntry entry;
if (AutoSyncID.ForDirectFileRequest.MOD_ID.equals(modID)){
return new Pair(hash, data); entry = AutoFileSyncEntry.ForDirectFileRequest.finishDeserializeContent(uniqueID, buf);
} } else {
entry = AutoFileSyncEntry.findMatching(modID, uniqueID);
private int serializeFileContent(FriendlyByteBuf buf) { }
byte[] content = getContent(); return new Triple<>(entry, data, new AutoSyncID(modID, uniqueID));
buf.writeInt(content.length); }
buf.writeByteArray(content);
return content.length;
} public void serialize(FriendlyByteBuf buf) {
getFileHash().serialize(buf);
private static byte[] deserializeFileContent(FriendlyByteBuf buf) { buf.writeBoolean(requestContent);
byte[] data;
int size = buf.readInt(); if (requestContent) {
data = buf.readByteArray(size); serializeFileContent(buf);
return data; }
} }
public static DataExchange.AutoSyncTriple deserializeAndMatch(FriendlyByteBuf buf) {
public static AutoFileSyncEntry findMatching(FileHash hash) { Pair<SyncFileHash, byte[]> e = deserialize(buf);
return findMatching(hash.modID, hash.uniqueID); AutoFileSyncEntry match = findMatching(e.first);
} return new DataExchange.AutoSyncTriple(e.first, e.second, match);
}
public static AutoFileSyncEntry findMatching(AutoSyncID aid) {
return findMatching(aid.modID, aid.uniqueID); public static Pair<SyncFileHash, byte[]> deserialize(FriendlyByteBuf buf) {
} SyncFileHash hash = SyncFileHash.deserialize(buf);
boolean withContent = buf.readBoolean();
public static AutoFileSyncEntry findMatching(String modID, String uniqueID) { byte[] data = null;
return DataExchange if (withContent) {
.getInstance() data = deserializeFileContent(buf);
.getAutoSyncFiles() }
.stream()
.filter(asf -> asf.modID.equals(modID) && asf.uniqueID.equals(uniqueID)) return new Pair(hash, data);
.findFirst() }
.orElse(null);
} 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; package ru.bclib.api.dataexchange.handler;
import net.minecraft.network.FriendlyByteBuf;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import ru.bclib.api.dataexchange.DataHandler;
import java.io.File; import java.io.File;
import java.util.Objects; import java.util.Objects;
@ -16,6 +18,27 @@ public class AutoSyncID {
this.localFile = localFile; 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. * A Unique ID for the referenced File.
* <p> * <p>
@ -56,4 +79,20 @@ public class AutoSyncID {
public int hashCode() { public int hashCode() {
return Objects.hash(uniqueID, modID); 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.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.loader.api.FabricLoader; 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.ConnectorClientside;
import ru.bclib.api.dataexchange.ConnectorServerside; import ru.bclib.api.dataexchange.ConnectorServerside;
import ru.bclib.api.dataexchange.DataExchangeAPI; import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.dataexchange.DataHandler; import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor; import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.api.dataexchange.FileHash; 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 ru.bclib.config.Configs;
import java.io.File; import java.io.File;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Stream;
abstract public class DataExchange { abstract public class DataExchange {
public final static Path SYNC_FOLDER = FabricLoader.getInstance() public final static SyncFolderDescriptor SYNC_FOLDER = new SyncFolderDescriptor("BCLIB-SYNC", FabricLoader.getInstance()
.getGameDir() .getGameDir()
.resolve("bclib-sync") .resolve("bclib-sync")
.toAbsolutePath(); .toAbsolutePath(), true);
public final static String SYNC_FOLDER_ID = "BCLIB-SYNC"; final List<SyncFolderDescriptor> syncFolderDescriptions = Arrays.asList(SYNC_FOLDER);
@FunctionalInterface public static class SyncFolderDescriptor {
public interface NeedTransferPredicate { static class SubFile {
public boolean test(FileHash clientHash, FileHash serverHash, FileContentWrapper content); public final String relPath;
} public final FileHash hash;
protected final static List<BiConsumer<AutoSyncID, File>> onWriteCallbacks = new ArrayList<>(2);
SubFile(String relPath, FileHash hash) {
final static class AutoSyncTriple { this.relPath = relPath;
public final FileHash serverHash; this.hash = hash;
public final byte[] serverContent; }
public final AutoFileSyncEntry localMatch;
@Override
public AutoSyncTriple(FileHash serverHash, byte[] serverContent, AutoFileSyncEntry localMatch) { public String toString() {
this.serverHash = serverHash; return relPath;
this.serverContent = serverContent; }
this.localMatch = localMatch;
} public void serialize(FriendlyByteBuf buf) {
DataHandler.writeString(buf, relPath);
@Override hash.serialize(buf);
public String toString() { }
return serverHash.modID+"."+serverHash.uniqueID;
} public static SubFile deserialize(FriendlyByteBuf buf) {
} final String relPath = DataHandler.readString(buf);
FileHash hash = FileHash.deserialize(buf);
private static DataExchangeAPI instance; return new SubFile(relPath, hash);
protected static DataExchangeAPI getInstance(){ }
if (instance==null){
instance = new DataExchangeAPI(); @Override
} public boolean equals(Object o) {
return instance; if (this == o) return true;
} if (o instanceof String) return relPath.equals(o);
if (!(o instanceof SubFile)) return false;
protected ConnectorServerside server; SubFile subFile = (SubFile) o;
protected ConnectorClientside client; return relPath.equals(subFile.relPath);
protected final Set<DataHandlerDescriptor> descriptors; }
private final List<AutoFileSyncEntry> autoSyncFiles = new ArrayList<>(4);
@Override
private boolean didLoadSyncFolder = false; public int hashCode() {
return relPath.hashCode();
abstract protected ConnectorClientside clientSupplier(DataExchange api); }
abstract protected ConnectorServerside serverSupplier(DataExchange api); }
protected DataExchange(){ @NotNull
descriptors = new HashSet<>(); public final String folderID;
} public final boolean removeAdditionalFiles;
@NotNull
public Set<DataHandlerDescriptor> getDescriptors() { return descriptors; } public final Path localFolder;
public List<AutoFileSyncEntry> getAutoSyncFiles(){ private List<SubFile> fileCache;
return autoSyncFiles;
} SyncFolderDescriptor(String folderID, Path localFolder, boolean removeAdditionalFiles) {
this.removeAdditionalFiles = removeAdditionalFiles;
@Environment(EnvType.CLIENT) this.folderID = folderID;
protected void initClientside(){ this.localFolder = localFolder;
if (client!=null) return; fileCache = null;
client = clientSupplier(this); }
@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) ->{ /*ClientLoginConnectionEvents.INIT.register((a, b) ->{
System.out.println("INIT"); System.out.println("INIT");
}); });
ClientLoginConnectionEvents.QUERY_START.register((a, b) ->{ ClientLoginConnectionEvents.QUERY_START.register((a, b) ->{
System.out.println("INIT"); System.out.println("INIT");
});*/ });*/
ClientPlayConnectionEvents.INIT.register(client::onPlayInit); ClientPlayConnectionEvents.INIT.register(client::onPlayInit);
ClientPlayConnectionEvents.JOIN.register(client::onPlayReady); ClientPlayConnectionEvents.JOIN.register(client::onPlayReady);
ClientPlayConnectionEvents.DISCONNECT.register(client::onPlayDisconnect); ClientPlayConnectionEvents.DISCONNECT.register(client::onPlayDisconnect);
} }
protected void initServerSide(){ protected void initServerSide() {
if (server!=null) return; if (server != null) return;
server = serverSupplier(this); server = serverSupplier(this);
ServerPlayConnectionEvents.INIT.register(server::onPlayInit); ServerPlayConnectionEvents.INIT.register(server::onPlayInit);
ServerPlayConnectionEvents.JOIN.register(server::onPlayReady); ServerPlayConnectionEvents.JOIN.register(server::onPlayReady);
ServerPlayConnectionEvents.DISCONNECT.register(server::onPlayDisconnect); ServerPlayConnectionEvents.DISCONNECT.register(server::onPlayDisconnect);
} }
/** /**
* Initializes all datastructures that need to exist in the client component. * Initializes all datastructures that need to exist in the client component.
* <p> * <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called * This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/ */
@Environment(EnvType.CLIENT) @Environment(EnvType.CLIENT)
public static void prepareClientside(){ public static void prepareClientside() {
DataExchange api = DataExchange.getInstance(); DataExchange api = DataExchange.getInstance();
api.initClientside(); api.initClientside();
} }
/** /**
* Initializes all datastructures that need to exist in the server component. * Initializes all datastructures that need to exist in the server component.
* <p> * <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called * This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/ */
public static void prepareServerside(){ public static void prepareServerside() {
DataExchange api = DataExchange.getInstance(); DataExchange api = DataExchange.getInstance();
api.initServerSide(); api.initServerSide();
} }
/**
/** * Automatically called before the player enters the world.
* Automatically called before the player enters the world. * <p>
* <p> * This is automatically called by BCLib. It will send all {@link DataHandler}-Objects that have {@link DataHandlerDescriptor#sendBeforeEnter} set to*
* This is automatically called by BCLib. It will send all {@link DataHandler}-Objects that have {@link DataHandlerDescriptor#sendBeforeEnter} set to* * {@code true},
* {@code true}, */
*/ @Environment(EnvType.CLIENT)
@Environment(EnvType.CLIENT) public static void sendOnEnter() {
public static void sendOnEnter(){ getInstance().descriptors.forEach((desc) -> {
getInstance().descriptors.forEach((desc)-> { if (desc.sendBeforeEnter) {
if (desc.sendBeforeEnter){ DataHandler h = desc.JOIN_INSTANCE.get();
DataHandler h = desc.JOIN_INSTANCE.get(); if (!h.getOriginatesOnServer()) {
if (!h.getOriginatesOnServer()) { getInstance().client.sendToServer(h);
getInstance().client.sendToServer(h); }
} }
} });
}); }
}
/**
/** * Registers a File for automatic client syncing.
* Registers a File for automatic client syncing. *
* * @param modID The ID of the calling Mod
* @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 needTransfer If the predicate returns true, the file needs to get copied to the server. * @param fileName The name of the File
* @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
* @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.
* entire file from the client to the server. * <p>
* <p> * You should only use this option, if you need to compare parts of the file in order to decide
* 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}
* If the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash} * for comparison is sufficient.
* for comparison is sufficient. */
*/ protected void addAutoSyncFileData(String modID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer) {
protected void addAutoSyncFileData(String modID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer){ autoSyncFiles.add(new AutoFileSyncEntry(modID, fileName, requestContent, needTransfer));
autoSyncFiles.add(new AutoFileSyncEntry(modID, fileName, requestContent, needTransfer)); }
}
/**
/** * Registers a File for automatic client syncing.
* Registers a File for automatic client syncing. *
* * @param modID The ID of the calling Mod
* @param modID The ID of the calling Mod * @param uniqueID A unique Identifier for the File. (see {@link SyncFileHash#uniqueID} for
* @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for * Details
* Details * @param needTransfer If the predicate returns true, the file needs to get copied to the server.
* @param needTransfer If the predicate returns true, the file needs to get copied to the server. * @param fileName The name of the File
* @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
* @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.
* entire file from the client to the server. * <p>
* <p> * You should only use this option, if you need to compare parts of the file in order to decide
* 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}
* If the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash} * for comparison is sufficient.
* for comparison is sufficient. */
*/ protected void addAutoSyncFileData(String modID, String uniqueID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer) {
protected void addAutoSyncFileData(String modID, String uniqueID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer){ autoSyncFiles.add(new AutoFileSyncEntry(modID, uniqueID, fileName, requestContent, 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.
* Called when {@code SendFiles} received a File on the Client and wrote it to the FileSystem. * <p>
* <p> * This is the place where reload Code should go.
* This is the place where reload Code should go. *
* @param aid The ID of the received File * @param aid The ID of the received File
* @param file The location of the FIle on the client * @param file The location of the FIle on the client
*/ */
static void didReceiveFile(AutoSyncID aid, File file){ static void didReceiveFile(AutoSyncID aid, File file) {
onWriteCallbacks.forEach(fkt -> fkt.accept(aid, file)); onWriteCallbacks.forEach(fkt -> fkt.accept(aid, file));
} }
private List<String> syncFolderContent; private List<String> syncFolderContent;
protected List<String> getSyncFolderContent(){
if (syncFolderContent==null){ protected List<String> getSyncFolderContent() {
return new ArrayList<>(0); if (syncFolderContent == null) {
} return new ArrayList<>(0);
return syncFolderContent; }
} return syncFolderContent;
}
//we call this from HelloServer to prepare transfer
protected void loadSyncFolder() { //we call this from HelloServer to prepare transfer
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offserSyncFolder", true)) protected void loadSyncFolder() {
{ if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offersSyncFolders", true)) {
final File syncPath = SYNC_FOLDER.toFile(); syncFolderDescriptions.forEach(desc -> desc.loadCache());
if (!syncPath.exists()) { }
syncPath.mkdirs(); }
}
protected static SyncFolderDescriptor getSyncFolderDescriptor(String folderID) {
if (syncFolderContent == null) { return ((DataExchange) getInstance()).syncFolderDescriptions.stream()
syncFolderContent = new ArrayList<>(8); .filter(d -> d.equals(folderID))
addFilesForSyncFolder(syncPath); .findFirst()
} .orElse(null);
} }
}
protected static Path localBasePathForFolderID(String folderID) {
private void addFilesForSyncFolder(File path){ final SyncFolderDescriptor desc = getSyncFolderDescriptor(folderID);
for (final File f : path.listFiles()) { if (desc != null) {
if (f.isDirectory()) { return desc.localFolder;
addFilesForSyncFolder(f); }
} else if (f.isFile()) { else {
if (!f.getName().startsWith(".")) { BCLib.LOGGER.warning("Unknown Sync-Folder ID '" + folderID + "'");
Path p = f.toPath(); return null;
p = SYNC_FOLDER.relativize(p); }
syncFolderContent.add(p.toString()); }
}
} 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.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor; import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.api.dataexchange.handler.AutoSyncID.WithContentOverride; 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.api.datafixer.DataFixerAPI;
import ru.bclib.config.Configs; import ru.bclib.config.Configs;
import ru.bclib.gui.screens.SyncFilesScreen; import ru.bclib.gui.screens.SyncFilesScreen;
import ru.bclib.gui.screens.WarnBCLibVersionMismatch; import ru.bclib.gui.screens.WarnBCLibVersionMismatch;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -41,28 +44,31 @@ public class HelloClient extends DataHandler {
super(DESCRIPTOR.IDENTIFIER, true); super(DESCRIPTOR.IDENTIFIER, true);
} }
public static String getModVersion(String modID){ public static String getModVersion(String modID) {
Optional<ModContainer> optional = FabricLoader.getInstance().getModContainer(modID); Optional<ModContainer> optional = FabricLoader.getInstance()
.getModContainer(modID);
if (optional.isPresent()) { if (optional.isPresent()) {
ModContainer modContainer = optional.get(); ModContainer modContainer = optional.get();
return modContainer.getMetadata().getVersion().toString(); return modContainer.getMetadata()
.getVersion()
.toString();
} }
return "0.0.0"; return "0.0.0";
} }
static String getBCLibVersion(){ static String getBCLibVersion() {
return getModVersion(BCLib.MOD_ID); return getModVersion(BCLib.MOD_ID);
} }
@Override @Override
protected void serializeData(FriendlyByteBuf buf) { protected void serializeData(FriendlyByteBuf buf) {
final String vbclib = getBCLibVersion(); 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(); final List<String> mods = DataExchangeAPI.registeredMods();
//write BCLibVersion (=protocol version) //write BCLibVersion (=protocol version)
buf.writeInt(DataFixerAPI.getModVersion(vbclib)); buf.writeInt(DataFixerAPI.getModVersion(vbclib));
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offerMods", true)) { if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offerMods", true)) {
//write Plugin Versions //write Plugin Versions
buf.writeInt(mods.size()); buf.writeInt(mods.size());
@ -72,128 +78,162 @@ public class HelloClient extends DataHandler {
buf.writeInt(DataFixerAPI.getModVersion(ver)); buf.writeInt(DataFixerAPI.getModVersion(ver));
BCLib.LOGGER.info(" - Listing Mod " + modID + " v" + ver); BCLib.LOGGER.info(" - Listing Mod " + modID + " v" + ver);
} }
} else { }
else {
BCLib.LOGGER.info("Server will not list Mods."); BCLib.LOGGER.info("Server will not list Mods.");
buf.writeInt(0); buf.writeInt(0);
} }
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offerFiles", true)) { if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offerFiles", true)) {
//do only include files that exist on the server //do only include files that exist on the server
final List<AutoFileSyncEntry> existingAutoSyncFiles = DataExchange final List<AutoFileSyncEntry> existingAutoSyncFiles = DataExchange.getInstance()
.getInstance() .getAutoSyncFiles()
.getAutoSyncFiles() .stream()
.stream() .filter(e -> e.fileName.exists())
.filter(e -> e.fileName.exists()) .collect(Collectors.toList());
.collect(Collectors.toList());
//send config Data //send config Data
buf.writeInt(existingAutoSyncFiles.size()); buf.writeInt(existingAutoSyncFiles.size());
for (AutoFileSyncEntry entry : existingAutoSyncFiles) { for (AutoFileSyncEntry entry : existingAutoSyncFiles) {
entry.serialize(buf); entry.serialize(buf);
BCLib.LOGGER.info(" - Offering File " + entry); BCLib.LOGGER.info(" - Offering File " + entry);
} }
} else { }
else {
BCLib.LOGGER.info("Server will not offer Files."); BCLib.LOGGER.info("Server will not offer Files.");
buf.writeInt(0); 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, "offersSyncFolders", true)) {
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offserSyncFolder", true)) { buf.writeInt(((DataExchange) DataExchange.getInstance()).syncFolderDescriptions.size());
buf.writeInt(1); //currently we do only sync a single folder ((DataExchange) DataExchange.getInstance()).syncFolderDescriptions.forEach(desc -> {
writeString(buf, DataExchange.SYNC_FOLDER_ID); //the UID of the Folder BCLib.LOGGER.info(" - Offering Folder " + desc.localFolder + " (allowDelete="+desc.removeAdditionalFiles+")");
final List<String> fileNames = DataExchange.getInstance().getSyncFolderContent(); desc.serialize(buf);
buf.writeInt(fileNames.size()); });
fileNames.forEach(fl -> writeString(buf, fl)); }
} else { else {
BCLib.LOGGER.info("Server will not offer Sync Folders."); BCLib.LOGGER.info("Server will not offer Sync Folders.");
buf.writeInt(0); buf.writeInt(0);
} }
Configs.MAIN_CONFIG.saveChanges(); Configs.MAIN_CONFIG.saveChanges();
} }
String bclibVersion ="0.0.0"; String bclibVersion = "0.0.0";
Map<String, String> modVersion = new HashMap<>(); Map<String, String> modVersion = new HashMap<>();
List<DataExchange.AutoSyncTriple> autoSyncedFiles = null; List<DataExchange.AutoSyncTriple> autoSyncedFiles = null;
List<SyncFolderDescriptor> autoSynFolders = null;
@Override @Override
protected void deserializeFromIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean fromClient) { protected void deserializeFromIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean fromClient) {
//read BCLibVersion (=protocol version) //read BCLibVersion (=protocol version)
bclibVersion = DataFixerAPI.getModVersion(buf.readInt()); bclibVersion = DataFixerAPI.getModVersion(buf.readInt());
//read Plugin Versions //read Plugin Versions
modVersion = new HashMap<>(); modVersion = new HashMap<>();
int count = buf.readInt(); int count = buf.readInt();
for (int i=0; i< count; i++) { for (int i = 0; i < count; i++) {
String id = readString(buf); String id = readString(buf);
String version = DataFixerAPI.getModVersion(buf.readInt()); String version = DataFixerAPI.getModVersion(buf.readInt());
modVersion.put(id, version); modVersion.put(id, version);
} }
//read config Data //read config Data
count = buf.readInt(); count = buf.readInt();
autoSyncedFiles = new ArrayList<>(count); autoSyncedFiles = new ArrayList<>(count);
for (int i=0; i< count; i++) { for (int i = 0; i < count; i++) {
//System.out.println("Deserializing "); //System.out.println("Deserializing ");
DataExchange.AutoSyncTriple t = AutoFileSyncEntry.deserializeAndMatch(buf); DataExchange.AutoSyncTriple t = AutoFileSyncEntry.deserializeAndMatch(buf);
autoSyncedFiles.add(t); autoSyncedFiles.add(t);
//System.out.println(t.first); //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")) { if (DataFixerAPI.isLargerOrEqualVersion(bclibVersion, "0.4.1")) {
final int folderCount = buf.readInt(); final int folderCount = buf.readInt();
for (int i=0; i<folderCount; i++){ for (int i = 0; i < folderCount; i++) {
final String folderID = readString(buf); SyncFolderDescriptor desc = SyncFolderDescriptor.deserialize(buf);
final int entries = buf.readInt(); autoSynFolders.add(desc);
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+"'");
}
} }
} }
} }
private void processAutoSyncFolder(final List<AutoSyncID> filesToRequest, final List<AutoSyncID.ForDirectFileRequest> filesToRemove) {
@Override if (!Configs.CLIENT_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "syncFolders", true)) {
protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient) { return;
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+")");
} }
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:"); 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) { for (DataExchange.AutoSyncTriple e : autoSyncedFiles) {
String actionString = ""; String actionString = "";
FileContentWrapper contentWrapper = new FileContentWrapper(e.serverContent); FileContentWrapper contentWrapper = new FileContentWrapper(e.serverContent);
if (e.localMatch == null) { if (e.localMatch == null) {
actionString = "(new, prepare update)"; actionString = "(new, prepare update)";
filesToRequest.add(new AutoSyncID(e.serverHash.modID, e.serverHash.uniqueID)); 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)"; actionString = "(prepare update)";
//we did not yet receive the new content //we did not yet receive the new content
if (contentWrapper.getRawContent() == null) { if (contentWrapper.getRawContent() == null) {
filesToRequest.add(new AutoSyncID(e.serverHash.modID, e.serverHash.uniqueID)); 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)); 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)); 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; return;
} }
} }
@Environment(EnvType.CLIENT) @Environment(EnvType.CLIENT)
protected void showBCLibError(Minecraft client){ protected void showBCLibError(Minecraft client) {
BCLib.LOGGER.error("BCLib differs on client and server."); BCLib.LOGGER.error("BCLib differs on client and server.");
client.setScreen(new WarnBCLibVersionMismatch((download) -> { client.setScreen(new WarnBCLibVersionMismatch((download) -> {
Minecraft.getInstance().setScreen((Screen)null); Minecraft.getInstance()
if (download){ .setScreen((Screen) null);
requestBCLibDownload((hadErrors)->{ if (download) {
requestBCLibDownload((hadErrors) -> {
client.stop(); client.stop();
}); });
} }
@ -226,38 +294,45 @@ public class HelloClient extends DataHandler {
} }
@Environment(EnvType.CLIENT) @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) -> { client.setScreen(new SyncFilesScreen((download) -> {
Minecraft.getInstance().setScreen((Screen)null); Minecraft.getInstance()
if (download){ .setScreen((Screen) null);
if (download) {
BCLib.LOGGER.info("Updating local Files:"); BCLib.LOGGER.info("Updating local Files:");
List<AutoSyncID.WithContentOverride> localChanges = new ArrayList<>(files.toArray().length); List<AutoSyncID.WithContentOverride> localChanges = new ArrayList<>(files.toArray().length);
List<AutoSyncID> requestFiles = new ArrayList<>(files.toArray().length); List<AutoSyncID> requestFiles = new ArrayList<>(files.toArray().length);
files.forEach(aid -> { files.forEach(aid -> {
if (aid instanceof WithContentOverride) { if (aid instanceof WithContentOverride) {
final WithContentOverride aidc = (WithContentOverride)aid; final WithContentOverride aidc = (WithContentOverride) aid;
BCLib.LOGGER.info(" - " + aid + " (updating Content)"); BCLib.LOGGER.info(" - " + aid + " (updating Content)");
SendFiles.writeSyncedFile(aid, aidc.contentWrapper.getRawContent(), aidc.localFile); SendFiles.writeSyncedFile(aid, aidc.contentWrapper.getRawContent(), aidc.localFile);
} else { }
else {
requestFiles.add(aid); requestFiles.add(aid);
BCLib.LOGGER.info(" - " + aid + " (requesting)"); BCLib.LOGGER.info(" - " + aid + " (requesting)");
} }
}); });
filesToRemove.forEach(aid -> {
BCLib.LOGGER.info(" - " + aid.relFile + " (removing)");
aid.relFile.delete();
});
requestFileDownloads(requestFiles); requestFileDownloads(requestFiles);
} }
})); }));
} }
private void requestBCLibDownload(Consumer<Boolean> whenFinished){ private void requestBCLibDownload(Consumer<Boolean> whenFinished) {
BCLib.LOGGER.warning("Starting download of BCLib"); BCLib.LOGGER.warning("Starting download of BCLib");
whenFinished.accept(true); whenFinished.accept(true);
} }
private void requestFileDownloads(List<AutoSyncID> files){ private void requestFileDownloads(List<AutoSyncID> files) {
BCLib.LOGGER.info("Starting download of Files:" + files.size()); BCLib.LOGGER.info("Starting download of Files:" + files.size());
DataExchangeAPI.send(new RequestFiles(files)); DataExchangeAPI.send(new RequestFiles(files));
} }

View file

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

View file

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

View file

@ -3,7 +3,7 @@ package ru.bclib.config;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import ru.bclib.BCLib; import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.DataExchangeAPI; 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.AutoSyncID;
import ru.bclib.api.dataexchange.handler.FileContentWrapper; import ru.bclib.api.dataexchange.handler.FileContentWrapper;
import ru.bclib.config.ConfigKeeper.BooleanEntry; 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); return keeper.compareAndUpdateForSync(content);
} }

View file

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