package net.scapeemulator.cache;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import net.scapeemulator.cache.util.FileChannelUtils;
/**
* A file store holds multiple files inside a "virtual" file system made up of
* several index files and a single data file.
* @author Graham
* @author `Discardedx2
*/
public final class FileStore implements Closeable {
public static FileStore create(String root, int indexes) throws IOException {
return create(new File(root), indexes);
}
public static FileStore create(File root, int indexes) throws IOException {
if (!root.mkdirs())
throw new IOException();
for (int i = 0; i < indexes; i++) {
File index = new File(root, "main_file_cache.idx" + i);
if (!index.createNewFile())
throw new IOException();
}
File meta = new File(root, "main_file_cache.idx255");
if (!meta.createNewFile())
throw new IOException();
File data = new File(root, "main_file_cache.dat2");
if (!data.createNewFile())
throw new IOException();
return open(root);
}
/**
* Opens the file store stored in the specified directory.
* @param root The directory containing the index and data files.
* @return The file store.
* @throws IOException if any of the {@code main_file_cache.*} files could
* not be opened.
*/
public static FileStore open(String root) throws IOException {
return open(new File(root));
}
/**
* Opens the file store stored in the specified directory.
* @param root The directory containing the index and data files.
* @return The file store.
* @throws IOException if any of the {@code main_file_cache.*} files could
* not be opened.
*/
public static FileStore open(File root) throws IOException {
File data = new File(root, "main_file_cache.dat2");
if (!data.exists())
throw new FileNotFoundException();
FileChannel dataChannel = FileChannel.open(data.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE);
List<FileChannel> indexChannels = new ArrayList<>();
for (int i = 0; i < 254; i++) {
File index = new File(root, "main_file_cache.idx" + i);
if (!index.exists())
break;
FileChannel indexChannel = FileChannel.open(index.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE);
indexChannels.add(indexChannel);
}
if (indexChannels.isEmpty())
throw new FileNotFoundException();
File meta = new File(root, "main_file_cache.idx255");
if (!meta.exists())
throw new FileNotFoundException();
FileChannel metaChannel = FileChannel.open(meta.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE);
return new FileStore(dataChannel, indexChannels.toArray(new FileChannel[0]), metaChannel);
}
/**
* The data file.
*/
private final FileChannel dataChannel;
/**
* The index files.
*/
private final FileChannel[] indexChannels;
/**
* The 'meta' index files.
*/
private final FileChannel metaChannel;
/**
* Creates a new file store.
* @param data The data file.
* @param indexes The index files.
* @param meta The 'meta' index file.
*/
public FileStore(FileChannel data, FileChannel[] indexes, FileChannel meta) {
this.dataChannel = data;
this.indexChannels = indexes;
this.metaChannel = meta;
}
/**
* Gets the number of index files, not including the meta index file.
* @return The number of index files.
* @throws IOException if an I/O error occurs.
*/
public int getTypeCount() throws IOException {
return indexChannels.length;
}
/**
* Gets the number of files of the specified type.
* @param type The type.
* @return The number of files.
* @throws IOException if an I/O error occurs.
*/
public int getFileCount(int type) throws IOException {
if ((type < 0 || type >= indexChannels.length) && type != 255)
throw new FileNotFoundException();
if (type == 255)
return (int) (metaChannel.size() / Index.SIZE);
return (int) (indexChannels[type].size() / Index.SIZE);
}
/**
* Writes a file.
* @param type The type of the file.
* @param id The id of the file.
* @param data A {@link ByteBuffer} containing the contents of the file.
* @throws IOException if an I/O error occurs.
*/
public void write(int type, int id, ByteBuffer data) throws IOException {
data.mark();
if (!write(type, id, data, true)) {
data.reset();
write(type, id, data, false);
}
}
/**
* Writes a file.
* @param type The type of the file.
* @param id The id of the file.
* @param data A {@link ByteBuffer} containing the contents of the file.
* @param overwrite A flag indicating if the existing file should be
* overwritten.
* @return A flag indicating if the file was written successfully.
* @throws IOException if an I/O error occurs.
*/
@SuppressWarnings("resource")
private boolean write(int type, int id, ByteBuffer data, boolean overwrite) throws IOException {
if ((type < 0 || type >= indexChannels.length) && type != 255)
throw new FileNotFoundException();
FileChannel indexChannel = type == 255 ? metaChannel : indexChannels[type];
int nextSector;
long ptr = id * Index.SIZE;
if (overwrite) {
if (ptr < 0)
throw new IOException();
else if (ptr >= indexChannel.size())
return false;
ByteBuffer buf = ByteBuffer.allocate(Index.SIZE);
FileChannelUtils.readFully(indexChannel, buf, ptr);
Index index = Index.decode((ByteBuffer) buf.flip());
nextSector = index.getSector();
if (nextSector <= 0 || nextSector > dataChannel.size() * Sector.SIZE)
return false;
} else {
nextSector = (int) ((dataChannel.size() + Sector.SIZE - 1) / Sector.SIZE);
if (nextSector == 0)
nextSector = 1;
}
Index index = new Index(data.remaining(), nextSector);
indexChannel.write(index.encode(), ptr);
ByteBuffer buf = ByteBuffer.allocate(Sector.SIZE);
int chunk = 0, remaining = index.getSize();
do {
int curSector = nextSector;
ptr = curSector * Sector.SIZE;
nextSector = 0;
if (overwrite) {
buf.clear();
FileChannelUtils.readFully(dataChannel, buf, ptr);
Sector sector = Sector.decode((ByteBuffer) buf.flip());
if (sector.getType() != type)
return false;
if (sector.getId() != id)
return false;
if (sector.getChunk() != chunk)
return false;
nextSector = sector.getNextSector();
if (nextSector < 0 || nextSector > dataChannel.size() / Sector.SIZE)
return false;
}
if (nextSector == 0) {
overwrite = false;
nextSector = (int) ((dataChannel.size() + Sector.SIZE - 1) / Sector.SIZE);
if (nextSector == 0)
nextSector++;
if (nextSector == curSector)
nextSector++;
}
byte[] bytes = new byte[Sector.DATA_SIZE];
if (remaining < Sector.DATA_SIZE) {
data.get(bytes, 0, remaining);
nextSector = 0; // mark as EOF
remaining = 0;
} else {
remaining -= Sector.DATA_SIZE;
data.get(bytes, 0, Sector.DATA_SIZE);
}
Sector sector = new Sector(type, id, chunk++, nextSector, bytes);
dataChannel.write(sector.encode(), ptr);
} while (remaining > 0);
return true;
}
/**
* Reads a file.
* @param type The type of the file.
* @param id The id of the file.
* @return A {@link ByteBuffer} containing the contents of the file.
* @throws IOException if an I/O error occurs.
*/
@SuppressWarnings("resource")
public ByteBuffer read(int type, int id) throws IOException {
if ((type < 0 || type >= indexChannels.length) && type != 255)
throw new FileNotFoundException();
FileChannel indexChannel = type == 255 ? metaChannel : indexChannels[type];
long ptr = id * Index.SIZE;
if (ptr < 0 || ptr >= indexChannel.size())
throw new FileNotFoundException();
ByteBuffer buf = ByteBuffer.allocate(Index.SIZE);
FileChannelUtils.readFully(indexChannel, buf, ptr);
Index index = Index.decode((ByteBuffer) buf.flip());
ByteBuffer data = ByteBuffer.allocate(index.getSize());
buf = ByteBuffer.allocate(Sector.SIZE);
int chunk = 0, remaining = index.getSize();
ptr = index.getSector() * Sector.SIZE;
do {
buf.clear();
FileChannelUtils.readFully(dataChannel, buf, ptr);
Sector sector = Sector.decode((ByteBuffer) buf.flip());
if (remaining > Sector.DATA_SIZE) {
data.put(sector.getData(), 0, Sector.DATA_SIZE);
remaining -= Sector.DATA_SIZE;
if (sector.getType() != type)
throw new IOException("File type mismatch.");
if (sector.getId() != id)
throw new IOException("File id mismatch.");
if (sector.getChunk() != chunk++)
throw new IOException("Chunk mismatch.");
ptr = sector.getNextSector() * Sector.SIZE;
} else {
data.put(sector.getData(), 0, remaining);
remaining = 0;
}
} while (remaining > 0);
return (ByteBuffer) data.flip();
}
@Override
public void close() throws IOException {
dataChannel.close();
for (FileChannel channel : indexChannels)
channel.close();
metaChannel.close();
}
}