helma/modules/jala/code/BitTorrent.js

429 lines
11 KiB
JavaScript

//
// Jala Project [http://opensvn.csie.org/traccgi/jala]
//
// Copyright 2004 ORF Online und Teletext GmbH
//
// Licensed under the Apache License, Version 2.0 (the ``License'');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an ``AS IS'' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// $Revision$
// $LastChangedBy$
// $LastChangedDate$
// $HeadURL$
//
/**
* @fileoverview Fields and methods of the jala.BitTorrent class.
*/
// Define the global namespace for Jala modules
if (!global.jala) {
global.jala = {};
}
/**
* Module dependencies
*/
app.addRepository("modules/core/String.js");
app.addRepository("modules/helma/File.js");
/**
* Constructs a new BitTorrent file.
* @class This class provides methods to create a BitTorrent
* metadata file from any desired file.
* @param {String} trackerUrl The URL string of the tracker.
* @param {String} filePath The path to the original file.
* @returns A new BitTorrent file.
* @constructor
*/
jala.BitTorrent = function(filePath, trackerUrl) {
var self = this;
self.arguments = arguments;
// FIXME: Add support for multitracker mode as specified in
// http://www.bittornado.com/docs/multitracker-spec.txt
var torrent, sourceFile, torrentFile;
var pieceLength = 256;
var updateTorrent = function() {
if (torrent.info) {
return torrent;
}
var file = new java.io.File(filePath);
if (!file.exists()) {
throw Error("File " + file + " does not exist!");
}
var md5 = java.security.MessageDigest.getInstance("MD5");
var sha1 = java.security.MessageDigest.getInstance("SHA-1");
var fis = new java.io.FileInputStream(file);
var bis = new java.io.BufferedInputStream(fis);
var cache = new java.io.ByteArrayOutputStream();
var pieces = [];
var length = pieceLength * 1024;
var buffer = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, length);
while (bis.read(buffer, 0, buffer.length) > -1) {
app.debug("Updating SHA-1 hash with " + buffer.length + " bytes");
sha1.reset();
sha1["update(byte[])"](buffer);
cache["write(byte[])"](buffer);
pieces.push(new java.lang.String(sha1.digest()));
}
bis.close();
fis.close();
torrent.info = {
//md5sum: new java.lang.String(md5.digest(cache.toByteArray())),
length: cache.size(),
name: file.getName(),
"piece length": length,
pieces: pieces.join("")
};
return torrent;
};
/**
* Get all available property names.
* @returns The list of property names.
* @type Array
*/
this.keys = function() {
var keys = [];
for (var i in torrent) {
keys.push(i);
}
keys.sort();
return keys;
};
/**
* Get a torrent property.
* @param {String} name The name of the property.
* @returns The value of the property.
*/
this.get = function(name) {
return torrent[name];
};
/**
* Set a torrent property.
* @param {String} name The name of the property.
* @param {Object} value The property's value.
*/
this.set = function(name, value) {
if (typeof torrent[name] == "undefined") {
throw Error("Cannot set torrent property " + name);
}
torrent[name] = value;
delete torrent.info;
return;
};
/**
* Get the creation date of the torrent.
* @returns The torrent's creation date.
* @type Date
*/
this.getCreationDate = function() {
return new Date(torrent["creation date"] * 1000);
};
/**
* Set the creation date of the torrent.
* @param {Date} date The desired creation date.
*/
this.setCreationDate = function(date) {
this.set("creation date", Math.round((date || new Date()).getTime() / 1000));
return;
};
/**
* Get the piece length of the torrent.
* @returns The torrent's piece length.
* @type Number
*/
this.getPieceLength = function() {
return pieceLength;
};
/**
* Set the piece length of the torrent.
* @param {Number} length The desired piece length.
*/
this.setPieceLength = function(length) {
pieceLength = length;
delete torrent.info;
return;
};
/**
* Returns the underlying torrent file.
* @returns The torrent file.
* @type helma.File
*/
this.getTorrentFile = function() {
return torrentFile;
};
/**
* Returns the underlying source file.
* @returns The source file.
* @type helma.File
*/
this.getSourceFile = function() {
return sourceFile;
};
/**
* Saves the torrent as file.
* @param {String} filename An optional name for the torrent file.
* If no name is given it will be composed from name of source
* file as defined in the torrent plus the ending ".torrent".
*/
this.save = function(filename) {
updateTorrent();
if (!filename) {
filename = torrent.info.name + ".torrent";
}
torrentFile = new helma.File(sourceFile.getParent(), filename);
torrentFile.remove();
torrentFile.open();
torrentFile.write(jala.BitTorrent.bencode(torrent));
torrentFile.close();
return;
};
/**
* Get a string representation of the torrent.
* @returns The torrent as string.
* @type String
*/
this.toString = function() {
return "[jala.BitTorrent " + filePath + "]";
};
if (String(filePath).endsWith(".torrent")) {
torrentFile = new helma.File(filePath);
torrent = jala.BitTorrent.bdecode(torrentFile.readAll());
sourceFile = new helma.File(torrent.info.name);
} else {
torrent = {
announce: trackerUrl || null,
"announce-list": null,
"creation date": null,
comment: null,
"created by": null,
};
this.setCreationDate();
sourceFile = new helma.File(filePath);
}
return this;
};
/**
* The bencode method. Turns an arbitrary JavaScript
* object structure into a corresponding encoded
* string.
* @param {Object} obj The target JavaScript object.
* @returns The encoded string.
* @type String
*/
jala.BitTorrent.bencode = function(obj) {
var bencode = arguments.callee;
var str = obj.toString();
res.push();
switch (obj.constructor) {
case Array:
res.write("l");
for (var i in obj) {
if (obj[i])
res.write(bencode(obj[i]));
}
res.write("e");
break;
case Number:
res.write("i" + str + "e");
break;
case Object:
res.write("d");
var keys = [];
for (var i in obj) {
keys.push(i);
}
keys.sort();
var key;
for (var i in keys) {
key = keys[i];
if (obj[key]) {
res.write(bencode(key));
res.write(bencode(obj[key]));
}
}
res.write("e");
break;
default:
res.write(str.length + ":" + str);
}
return res.pop();
};
/**
* The bdecode method. Turns an encoded string into
* a corresponding JavaScript object structure.
* FIXME: Handle with caution...
* @param {String} code The encoded string.
* @returns The decoded JavaScript structure.
* @type Object
*/
jala.BitTorrent.bdecode = function(code) {
var DICTIONARY = "d";
var LIST = "l";
var INTEGER = "i";
var STRING = "s";
var END = "e";
var stack = [];
var overflowCounter = 0;
var position = -1, current;
function getResult() {
update();
var result;
switch (current) {
case DICTIONARY:
result = bdecodeDictionary();
break;
case LIST:
result = bdecodeList();
break;
case INTEGER:
result = bdecodeInteger();
break;
case END:
case null:
//res.debug("*** end detected in getResult()");
result = null;
break;
default:
result = bdecodeString();
}
return result;
}
function update() {
position += 1;
current = code.charAt(position);
/* res.debug("stack: " + stack);
res.debug("position: " + position);
res.debug("current: " + current);
res.debug("remains: " + code.substr(position)); */
return;
}
function overflow() {
if (overflowCounter++ > 100)
throw Error("Error parsing bdecoded string");
return false;
}
function bdecodeDictionary() {
stack.push(DICTIONARY);
var dictionary = {}, key, value;
while (current && !overflow()) {
key = getResult();
if (key === null)
break;
value = getResult();
if (key && value)
dictionary[key] = value;
else
break;
}
stack.pop();
return dictionary;
}
function bdecodeList() {
stack.push(LIST);
var list = [], value;
while (current && !overflow()) {
var value = getResult();
if (value)
list.push(value);
else
break;
}
stack.pop();
return list;
}
function bdecodeInteger() {
var integer = "";
stack.push(integer);
while (current && !overflow()) {
update();
if (current == "e")
break;
integer += current;
}
if (isNaN(integer))
throw("Error in bdecoded integer: " + integer + " is not a number");
//res.debug("integer = " + integer);
stack.pop();
return parseInt(integer);
}
function bdecodeString() {
var length = current, string = "";
stack.push(string);
update();
while (current && current != ":" && !overflow()) {
length += current;
update();
}
if (isNaN(length))
throw("Error in bdecoded string: invalid length " + length);
//res.debug("length = " + length);
length = parseInt(length);
if (length > code.length - position)
throw Error("Error parsing bdecoded string");
for (var i=0; i<length; i+=1) {
update();
string += current;
}
//res.debug("string = " + string);
if (string == "creation date")
void(null);
stack.pop();
return string;
}
return getResult();
};