begin actual network client work (file management)

master
rarkenin 2014-04-26 18:32:32 -04:00
parent 0941297e42
commit 4cf56430e7
8 changed files with 355 additions and 49 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="false">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
@ -10,7 +10,6 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: com.google.guava:guava:15.0" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-math:2.2" level="project" />
<orderEntry type="library" name="Maven: commons-lang:commons-lang:2.6" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-lang3:3.1" level="project" />

View File

@ -0,0 +1,9 @@
package net.mosstest.client;
/**
* Created by hexafraction on 4/26/14.
*/
public class MossClient {
RemoteFileManager fileManager;
}

View File

@ -0,0 +1,277 @@
package net.mosstest.client;
import com.jme3.asset.*;
import net.mosstest.servercore.*;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.apache.log4j.Logger;
import java.io.*;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.*;
/**
* Created by hexafraction on 4/26/14.
*/
public class RemoteFileManager implements IFileManager {
private static final String XML_DEPENDENCY_KEY = "dependencies.dependency";
private static volatile RemoteFileManager instance = null;
private HashSet<String> visitedScripts;
protected HashSet<AbstractMossScript> executed = new HashSet<>();
public static RemoteFileManager getInstance() {
if (instance == null) {
synchronized (RemoteFileManager.class) {
if (instance == null) {
instance = new RemoteFileManager();
}
}
}
return instance;
}
private static final Logger logger = Logger.getLogger(RemoteFileManager.class);
private HashMap<String, RemoteFile> knownByName = new HashMap<>();
private HashMap<String, RemoteFile> knownByHash = new HashMap<>();
private File cacheBasedir;
// used to request files
private MossClient client;
@Override
public RemoteFile getFile(String name) throws IOException {
return knownByName.get(name);
}
@Override
public void registerFile(String name, String sha256, long size) throws IOException {
logger.info(MessageFormat.format("Registered {0} with hash {1}, and a size of {2} bytes", name, sha256, size));
RemoteFile rmf = new RemoteFile(sha256, name, size);
knownByName.put(name, rmf);
knownByHash.put(sha256, rmf);
}
@Override
public void receiveFileChunk(String sha256, int chunkId, ByteBuffer buf) throws IOException {
RemoteFile rmf = this.knownByHash.get(sha256);
if (rmf == null) {
logger.warn(MessageFormat.format("The server tried to send a file chunk for a file with hash {0} that we don't know about.", sha256));
return;
}
rmf.writeChunk(chunkId, buf.array());
}
@Override
public Class<? extends AssetLocator> getAssetLocatorClass() {
return RemoteAssetLocator.class;
}
@Override
public List<? extends IMossFile> getFiles() {
return null;
}
@Override
public AbstractMossScript getScript(String name) throws IOException, MossWorldLoadException {
List<AbstractMossScript> dependencies = new ArrayList<>();
try {
File scriptXml = this.getFile(name + "/script.xml").file;
XMLConfiguration scriptCfg = new XMLConfiguration(scriptXml);
String[] scNames = scriptCfg.getStringArray(XML_DEPENDENCY_KEY);
for (String sc : scNames) {
if (sc.equals(name)) continue;
if (visitedScripts.contains(sc)) {
logger.fatal(MessageFormat.format(Messages.getString("CIRCULAR_DEPENDENCY_ISSUE"), name, sc));
throw new MossWorldLoadException(MessageFormat.format(Messages.getString("CIRCULAR_DEPENDENCY_ISSUE"), name, sc));
}
try {
dependencies.add(this.getScript(sc));
} catch (StackOverflowError e) {
// should never happen
logger.fatal(Messages.getString("DEPFIND_STACK_OVERFLOW"));
throw new MossWorldLoadException("FIXME The stack overflowed while resolving dependencies. Either there is an undetected dependency issue, or there is simply an extreme number of dependencies.");
}
}
} catch (ConfigurationException | FileNotFoundException e) {
logger.warn(Messages.getString("SCRIPT_XML_MISSING"));
}
return new RemoteScript(name, dependencies);
}
public class RemoteFile implements IMossFile {
private final long size;
private final File file;
private final RandomAccessFile rFile;
private final BitSet completedChunks;
private final String sha256;
private final int numChunks;
private final String name;
RemoteFile(String sha256, String name, long size) throws IOException {
this.sha256 = sha256;
this.name = name;
numChunks = (int) Math.ceil(size / IMossFile.CHUNK_SIZE);
if (!sha256.matches("[0-9A-Za-z]{32}"))
throw new MosstestFatalDeathException("A file SHA256 was invalid, and could not be used for caching.");
this.file = new File(RemoteFileManager.this.cacheBasedir, sha256);
this.file.createNewFile();
this.rFile = new RandomAccessFile(this.file, "rw");
this.size = size;
try {
completedChunks = new BitSet(numChunks);
String actualSha = IFileManager.getHash(file);
if (actualSha.equalsIgnoreCase(sha256)) {
completedChunks.set(0, numChunks);
}
} catch (NoSuchAlgorithmException e) {
throw new MosstestFatalDeathException("The SHA-256 algorithm could not be found, but is required to cache files.");
}
}
@Override
public Reader getReader() throws FileNotFoundException {
return new FileReader(this.file);
}
@Override
public InputStream getInputStream() throws FileNotFoundException {
return new FileInputStream(this.file);
}
@Override
public byte[] readChunk(int chunkId) throws IOException {
this.rFile.seek(chunkId * IMossFile.CHUNK_SIZE);
byte[] buf = new byte[(int) (chunkId == (this.numChunks - 1) ? (this.size % IMossFile.CHUNK_SIZE)
: IMossFile.CHUNK_SIZE)];
this.rFile.readFully(buf);
return buf;
}
@Override
public void writeChunk(int chunkId, byte[] buf) throws IOException {
this.rFile.seek(chunkId * IMossFile.CHUNK_SIZE);
if (((chunkId == (this.numChunks - 1) ? (this.size % IMossFile.CHUNK_SIZE) : IMossFile.CHUNK_SIZE)) != buf.length)
throw new MosstestFatalDeathException(MessageFormat.format("An inbound chunk for file {0} is not of the correct size. The server may be sending erroneous data.", this.name));
this.completedChunks.set(chunkId);
if (isComplete()) {
logger.info(MessageFormat.format("Completed file {0}.", this.name));
}
try {
String realHash = IFileManager.getHash(this.file);
if (!this.sha256.equalsIgnoreCase(realHash)) {
logger.warn(MessageFormat.format("File {0} expected with hash {1} but got reassembled as hash {2}", this.name, this.sha256, realHash));
}
} catch (NoSuchAlgorithmException e) {
throw new MosstestFatalDeathException("The SHA-256 algorithm could not be found, but is required to cache files.");
}
}
@Override
public String getSha256() {
return this.sha256;
}
@Override
public String getName() {
return this.name;
}
@Override
public boolean isComplete() {
return (completedChunks.nextClearBit(0) == -1 || completedChunks.nextClearBit(0) == this.numChunks);
}
@Override
public long getSize() {
return this.size;
}
}
private static class RemoteAssetLocator implements AssetLocator {
private static final Logger logger = Logger.getLogger(RemoteAssetLocator.class);
@Override
public void setRootPath(String s) {
// ignore
logger.warn(MessageFormat.format("Tried to set root path {0} for RemoteAssetLocator, which ignores the root path.", s));
}
@Override
public AssetInfo locate(AssetManager assetManager, AssetKey assetKey) {
try {
return new RemoteAssetInfo(assetManager, assetKey,
RemoteFileManager.getInstance().getFile(assetKey.getName()));
} catch (IOException ex) {
throw new AssetLoadException("Failed to open file: "
+ assetKey.getName(), ex);
}
}
private static class RemoteAssetInfo extends AssetInfo {
private RemoteFile file;
public RemoteAssetInfo(AssetManager manager, AssetKey key, RemoteFile file) {
super(manager, key);
this.file = file;
}
@Override
public InputStream openStream() {
try {
return this.file.getInputStream();
} catch (IOException ex) {
throw new AssetLoadException("Failed to open file: "
+ this.file.getName(), ex);
}
}
}
}
private class RemoteScript extends AbstractMossScript {
private final List<AbstractMossScript> dependencies;
public RemoteScript(String name, List<AbstractMossScript> dependencies) {
super(name);
this.dependencies = dependencies;
}
@Override
public void exec(ScriptEnv sEnv) throws IOException, MossWorldLoadException {
RemoteFileManager.this.executed.add(this);
for (AbstractMossScript sc : dependencies) {
sc.exec(sEnv);
}
sEnv.runScript(this.getInitFile());
}
@Override
public IMossFile getInitFile() throws IOException {
return RemoteFileManager.this.getFile(this.name + "/init.js");
}
@Override
public List<AbstractMossScript> getDependencies() {
return Collections.unmodifiableList(dependencies);
}
}
}

View File

@ -9,12 +9,12 @@ public abstract class AbstractMossScript {
this.name = name;
}
final String name;
abstract void exec(ScriptEnv sEnv) throws IOException, MossWorldLoadException;
protected final String name;
public abstract void exec(ScriptEnv sEnv) throws IOException, MossWorldLoadException;
abstract IMossFile getInitFile() throws IOException;
public abstract IMossFile getInitFile() throws IOException;
abstract List<AbstractMossScript> getDependencies();
public abstract List<AbstractMossScript> getDependencies();
@Override
public final boolean equals(Object o) {

View File

@ -2,16 +2,61 @@ package net.mosstest.servercore;
import com.jme3.asset.AssetLocator;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
public interface IFileManager {
static String getHash(File f) throws IOException,
NoSuchAlgorithmException, FileNotFoundException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (FileInputStream fis = new FileInputStream(f)) {
try (FileChannel fc = fis.getChannel()) {
ByteBuffer bbf = ByteBuffer.allocateDirect(LocalFileManager.HASHING_BUFFER_SIZE);
int bytesRead;
bytesRead = fc.read(bbf);
while ((bytesRead != -1) && (bytesRead != 0)) {
bbf.flip();
md.update(bbf);
bbf.clear();
bytesRead = fc.read(bbf);
}
fis.close();
byte[] mdBytes = md.digest();
StringBuilder hexString = new StringBuilder();
for (byte b : mdBytes) {
hexString.append(Integer.toHexString((LocalFileManager.BYTE_CAST_MASK & b)));
}
return hexString.toString();
}
}
}
public IMossFile getFile(String name) throws IOException;
public void registerFile(String name, String sha256, long size);
public void registerFile(String name, String sha256, long size) throws IOException;
public void receiveFileChunk(String sha512, int chunkId, ByteBuffer buf) throws IOException;
public void receiveFileChunk(String sha256, int chunkId, ByteBuffer buf) throws IOException;
public Class<? extends AssetLocator> getAssetLocatorClass();

View File

@ -19,4 +19,8 @@ public interface IMossFile {
public String getSha256();
public String getName();
public boolean isComplete();
public long getSize();
}

View File

@ -33,6 +33,17 @@ public class LocalFile implements IMossFile {
return name;
}
@Override
public boolean isComplete() {
// local files are always complete
return true;
}
@Override
public long getSize() {
return f.length();
}
private final String sha256;
public LocalFile(String name, File f) throws IOException, FileNotFoundException {
@ -46,7 +57,7 @@ public class LocalFile implements IMossFile {
this.numChunks = (int) Math.ceil(this.length
/ ((double) IMossFile.CHUNK_SIZE));
try {
this.sha256 = LocalFileManager.getHash(f);
this.sha256 = IFileManager.getHash(f);
logger.info("Hashed " + f.getAbsolutePath() + " as " + this.sha256);
} catch (NoSuchAlgorithmException e) {
logger.error("Could not find algorithm SHA-256 while hashing " + f.getAbsolutePath());

View File

@ -75,7 +75,7 @@ public class LocalFileManager implements IFileManager {
}
@Override
public void receiveFileChunk(String sha512, int chunkId, ByteBuffer buf) throws IOException {
public void receiveFileChunk(String sha256, int chunkId, ByteBuffer buf) throws IOException {
throw new IOException("This file is read-only due to its being in a non-cache directory.");
}
@ -89,45 +89,6 @@ public class LocalFileManager implements IFileManager {
}
public static String getHash(File f) throws IOException,
NoSuchAlgorithmException, FileNotFoundException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (FileInputStream fis = new FileInputStream(f)) {
try (FileChannel fc = fis.getChannel()) {
ByteBuffer bbf = ByteBuffer.allocateDirect(HASHING_BUFFER_SIZE);
int bytesRead;
bytesRead = fc.read(bbf);
while ((bytesRead != -1) && (bytesRead != 0)) {
bbf.flip();
md.update(bbf);
bbf.clear();
bytesRead = fc.read(bbf);
}
fis.close();
byte[] mdBytes = md.digest();
StringBuilder hexString = new StringBuilder();
for (byte b : mdBytes) {
hexString.append(Integer.toHexString((BYTE_CAST_MASK & b)));
}
return hexString.toString();
}
}
}
@Override
public List<? extends IMossFile> getFiles() {
return ImmutableList.copyOf(files.values());