private static String hashFiles(List<File> files)
		throws NoSuchAlgorithmException, InterruptedException, IOException {
		ExecutorService executor = Executors.newFixedThreadPool(
			getHashingThreadsCount());
		List<Future<String>> results = new LinkedList<Future<String>>();
		long length = 0L;

		ByteBuffer buffer = ByteBuffer.allocate(Torrent.PIECE_LENGTH);

		long start = System.nanoTime();
		for (File file : files) {
			logger.info("Analyzing local data for {} with {} threads...",
				file.getName(), getHashingThreadsCount());

			length += file.length();

			FileInputStream fis = new FileInputStream(file);
			FileChannel channel = fis.getChannel();

			while (channel.read(buffer) > 0) {
				if (buffer.remaining() == 0) {
					buffer.clear();
					results.add(executor.submit(new CallableChunkHasher(buffer)));
				}
			}

			channel.close();
			fis.close();
		}

		// Hash the last bit, if any
		if (buffer.position() > 0) {
			buffer.limit(buffer.position());
			buffer.position(0);
			results.add(executor.submit(new CallableChunkHasher(buffer)));
		}

		// Request orderly executor shutdown and wait for hashing tasks to
		// complete.
		executor.shutdown();
		while (!executor.isTerminated()) {
			Thread.sleep(10);
		}
		long elapsed = System.nanoTime() - start;

		StringBuffer hashes = new StringBuffer();
		try {
			for (Future<String> chunk : results) {
				hashes.append(chunk.get());
			}
		} catch (ExecutionException ee) {
			throw new IOException("Error while hashing the torrent data!", ee);
		}

		int expectedPieces = (int) (Math.ceil(
				(double)length / Torrent.PIECE_LENGTH));
		logger.info("Hashed {} file(s) ({} bytes) in {} pieces ({} expected) in {}ms.",
			new Object[] {
				files.size(),
				length,
				results.size(),
				expectedPieces,
				String.format("%.1f", elapsed/1024.0/1024.0),
			});

		return hashes.toString();
	}