/* * Storage - Class used to store and retrieve pieces. Copyright (C) 2003 Mark J. * Wielaard * * This file is part of Snark. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 59 Temple * Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.klomp.snark; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; /** * Maintains pieces on disk. Can be used to store and retrieve pieces. */ public class Storage { private MetaInfo metainfo; private long[] lengths; private RandomAccessFile[] rafs; private String[] names; private final StorageListener listener; private final BitField bitfield; private int needed; // XXX - Not always set correctly int piece_size; int pieces; /** The default piece size. */ private static int MIN_PIECE_SIZE = 256 * 1024; /** The maximum number of pieces in a torrent. */ private static long MAX_PIECES = 100 * 1024 / 20; /** * Creates a new storage based on the supplied MetaInfo. This will try to * create and/or check all needed files in the MetaInfo. * * @exception IOException * when creating and/or checking files fails. */ public Storage (MetaInfo metainfo, StorageListener listener) throws IOException { this.metainfo = metainfo; this.listener = listener; needed = metainfo.getPieces(); bitfield = new BitField(needed); } /** * Creates a storage from the existing file or directory together with an * appropriate MetaInfo file as can be announced on the given announce * String location. */ public Storage (File baseFile, String announce, StorageListener listener) throws IOException { this.listener = listener; // Create names, rafs and lengths arrays. getFiles(baseFile); long total = 0; ArrayList<Long> lengthsList = new ArrayList<Long>(); for (long length : lengths) { total += length; lengthsList.add(length); } piece_size = MIN_PIECE_SIZE; pieces = (int)((total - 1) / piece_size) + 1; while (pieces > MAX_PIECES) { piece_size = piece_size * 2; pieces = (int)((total - 1) / piece_size) + 1; } // Note that piece_hashes and the bitfield will be filled after // the MetaInfo is created. byte[] piece_hashes = new byte[20 * pieces]; bitfield = new BitField(pieces); needed = 0; List<List<String>> files = new ArrayList<List<String>>(); for (String element : names) { List<String> file = new ArrayList<String>(); StringTokenizer st = new StringTokenizer(element, File.separator); while (st.hasMoreTokens()) { String part = st.nextToken(); file.add(part); } files.add(file); } if (files.size() == 1) { files = null; lengthsList = null; } // Note that the piece_hashes are not correctly setup yet. metainfo = new MetaInfo(announce, null, baseFile.getName(), files, lengthsList, piece_size, piece_hashes, total); } // Creates piece hases for a new storage. public void create () throws IOException { // Calculate piece_hashes MessageDigest digest = null; try { digest = MessageDigest.getInstance("SHA"); } catch (NoSuchAlgorithmException nsa) { throw new InternalError(nsa.toString()); } byte[] piece_hashes = metainfo.getPieceHashes(); byte[] piece = new byte[piece_size]; for (int i = 0; i < pieces; i++) { int length = getUncheckedPiece(i, piece, 0); digest.update(piece, 0, length); byte[] hash = digest.digest(); for (int j = 0; j < 20; j++) { piece_hashes[20 * i + j] = hash[j]; } bitfield.set(i); if (listener != null) { listener.storageChecked(this, i, true); } } if (listener != null) { listener.storageAllChecked(this); } // Reannounce to force recalculating the info_hash. metainfo = metainfo.reannounce(metainfo.getAnnounce()); } private void getFiles (File base) throws IOException { ArrayList<File> files = new ArrayList<File>(); addFiles(files, base); int size = files.size(); names = new String[size]; lengths = new long[size]; rafs = new RandomAccessFile[size]; int i = 0; Iterator it = files.iterator(); while (it.hasNext()) { File f = (File)it.next(); names[i] = f.getPath(); lengths[i] = f.length(); rafs[i] = new RandomAccessFile(f, "r"); i++; } } private static void addFiles (List<File> l, File f) { if (!f.isDirectory()) { l.add(f); } else { File[] files = f.listFiles(); if (files == null) { log.log(Level.WARNING, "Skipping '" + f + "' not a normal file."); return; } for (File element : files) { addFiles(l, element); } } } /** * Returns the MetaInfo associated with this Storage. */ public MetaInfo getMetaInfo () { return metainfo; } /** * How many pieces are still missing from this storage. */ public int needed () { return needed; } /** * Whether or not this storage contains all pieces if the MetaInfo. */ public boolean complete () { return needed == 0; } /** * The BitField that tells which pieces this storage contains. Do not change * this since this is the current state of the storage. */ public BitField getBitField () { return bitfield; } /** * Creates (and/or checks) all files from the metainfo file list. */ public void check () throws IOException { File base = new File(filterName(metainfo.getName())); List files = metainfo.getFiles(); if (files == null) { // Create base as file. log.log(Level.INFO, "Creating/Checking file: " + base); if (!base.createNewFile() && !base.exists()) { throw new IOException("Could not create file " + base); } lengths = new long[1]; rafs = new RandomAccessFile[1]; names = new String[1]; lengths[0] = metainfo.getTotalLength(); rafs[0] = new RandomAccessFile(base, "rw"); names[0] = base.getName(); } else { // Create base as dir. log.log(Level.INFO, "Creating/Checking directory: " + base); if (!base.mkdir() && !base.isDirectory()) { throw new IOException("Could not create directory " + base); } List ls = metainfo.getLengths(); int size = files.size(); long total = 0; lengths = new long[size]; rafs = new RandomAccessFile[size]; names = new String[size]; for (int i = 0; i < size; i++) { File f = createFileFromNames(base, (List)files.get(i)); lengths[i] = ((Long)ls.get(i)).longValue(); total += lengths[i]; rafs[i] = new RandomAccessFile(f, "rw"); names[i] = f.getName(); } // Sanity check for metainfo file. long metalength = metainfo.getTotalLength(); if (total != metalength) { throw new IOException("File lengths do not add up " + total + " != " + metalength); } } checkCreateFiles(); } /** * Removes 'suspicious' characters from the give file name. */ private String filterName (String name) { // XXX - Is this enough? return name.replace(File.separatorChar, '_'); } private File createFileFromNames (File base, List names) throws IOException { File f = null; Iterator it = names.iterator(); while (it.hasNext()) { String name = filterName((String)it.next()); if (it.hasNext()) { // Another dir in the hierarchy. f = new File(base, name); if (!f.mkdir() && !f.isDirectory()) { throw new IOException("Could not create directory " + f); } base = f; } else { // The final element (file) in the hierarchy. f = new File(base, name); if (!f.createNewFile() && !f.exists()) { throw new IOException("Could not create file " + f); } } } return f; } private void checkCreateFiles () throws IOException { // Whether we are resuming or not, // if any of the files already exists we assume we are resuming. boolean resume = false; // Make sure all files are available and of correct length for (int i = 0; i < rafs.length; i++) { long length = rafs[i].length(); if (length == lengths[i]) { if (listener != null) { listener.storageAllocated(this, length); } resume = true; // XXX Could dynamicly check } else if (length == 0) { allocateFile(i); } else { log.log(Level.FINE, "Truncating '" + names[i] + "' from " + lengths + " to " + lengths[i] + "bytes"); rafs[i].setLength(lengths[i]); allocateFile(i); } } // Check which pieces match and which don't if (resume) { pieces = metainfo.getPieces(); byte[] piece = new byte[metainfo.getPieceLength(0)]; for (int i = 0; i < pieces; i++) { int length = getUncheckedPiece(i, piece, 0); boolean correctHash = metainfo.checkPiece(i, piece, 0, length); if (correctHash) { bitfield.set(i); needed--; } if (listener != null) { listener.storageChecked(this, i, correctHash); } } } if (listener != null) { listener.storageAllChecked(this); } } private void allocateFile (int nr) throws IOException { // XXX - Is this the best way to make sure we have enough space for // the whole file? listener.storageCreateFile(this, names[nr], lengths[nr]); final int ZEROBLOCKSIZE = metainfo.getPieceLength(0); byte[] zeros = new byte[ZEROBLOCKSIZE]; int i; for (i = 0; i < lengths[nr] / ZEROBLOCKSIZE; i++) { rafs[nr].write(zeros); if (listener != null) { listener.storageAllocated(this, ZEROBLOCKSIZE); } } int size = (int)(lengths[nr] - i * ZEROBLOCKSIZE); rafs[nr].write(zeros, 0, size); if (listener != null) { listener.storageAllocated(this, size); } } /** * Closes the Storage and makes sure that all RandomAccessFiles are closed. * The Storage is unusable after this. */ public void close () throws IOException { for (RandomAccessFile element : rafs) { synchronized (element) { element.close(); } } } /** * Returns a byte array containing the requested piece or null if the * storage doesn't contain the piece yet. */ public byte[] getPiece (int piece) throws IOException { if (!bitfield.get(piece)) { return null; } byte[] bs = new byte[metainfo.getPieceLength(piece)]; getUncheckedPiece(piece, bs, 0); return bs; } /** * Put the piece in the Storage if it is correct. * * @return true if the piece was correct (sha metainfo hash matches), * otherwise false. * @exception IOException * when some storage related error occurs. */ public boolean putPiece (int piece, byte[] bs) throws IOException { // First check if the piece is correct. // If we were paranoia we could copy the array first. int length = bs.length; boolean correctHash = metainfo.checkPiece(piece, bs, 0, length); if (listener != null) { listener.storageChecked(this, piece, correctHash); } if (!correctHash) { return false; } synchronized (bitfield) { if (bitfield.get(piece)) { return true; // No need to store twice. } else { bitfield.set(piece); needed--; } } long start = piece * metainfo.getPieceLength(0); int i = 0; long raflen = lengths[i]; while (start > raflen) { i++; start -= raflen; raflen = lengths[i]; } int written = 0; int off = 0; while (written < length) { int need = length - written; int len = (start + need < raflen) ? need : (int)(raflen - start); synchronized (rafs[i]) { rafs[i].seek(start); rafs[i].write(bs, off + written, len); } written += len; if (need - len > 0) { i++; raflen = lengths[i]; start = 0; } } return true; } private int getUncheckedPiece (int piece, byte[] bs, int off) throws IOException { // XXX - copy/paste code from putPiece(). long start = piece * metainfo.getPieceLength(0); int length = metainfo.getPieceLength(piece); int i = 0; long raflen = lengths[i]; while (start > raflen) { i++; start -= raflen; raflen = lengths[i]; } int read = 0; while (read < length) { int need = length - read; int len = (start + need < raflen) ? need : (int)(raflen - start); synchronized (rafs[i]) { rafs[i].seek(start); rafs[i].readFully(bs, off + read, len); } read += len; if (need - len > 0) { i++; raflen = lengths[i]; start = 0; } } return length; } /** The Java logger used to process our log events. */ protected static final Logger log = Logger.getLogger("org.klomp.snark.Storage"); }