diff --git a/.hgignore b/.hgignore index 4a420f3c..458cf85b 100644 --- a/.hgignore +++ b/.hgignore @@ -5,3 +5,4 @@ syntax: glob .metadata/ bin/ gen/ +lint.xml diff --git a/android/res/values/arrays.xml b/android/res/values/arrays.xml index 6e211da2..92949b84 100644 --- a/android/res/values/arrays.xml +++ b/android/res/values/arrays.xml @@ -3,6 +3,7 @@ + BitComet Bitflu 1.2+ BitTorrent 6+ Buffalo NAS -1.31 @@ -17,6 +18,7 @@ Vuze + daemon_bitcomet daemon_bitflu daemon_bittorrent daemon_buffalonas diff --git a/lib/src/com/android/internalcopy/http/multipart/BitCometFilePart.java b/lib/src/com/android/internalcopy/http/multipart/BitCometFilePart.java new file mode 100644 index 00000000..bc75ef38 --- /dev/null +++ b/lib/src/com/android/internalcopy/http/multipart/BitCometFilePart.java @@ -0,0 +1,238 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/FilePart.java,v 1.19 2004/04/18 23:51:37 jsdever Exp $ + * $Revision: 480424 $ + * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package com.android.internalcopy.http.multipart; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.apache.http.util.EncodingUtils; + +/** + * This class implements a part of a Multipart post object that + * consists of a file. + * + * @author Matthew Albright + * @author Jeff Dever + * @author Adrian Sutton + * @author Michael Becke + * @author Mark Diggory + * @author Mike Bowler + * @author Oleg Kalnichevski + * + * @since 2.0 + * + */ +public class BitCometFilePart extends PartBase { + + /** Default content encoding of file attachments. */ + public static final String DEFAULT_CONTENT_TYPE = "application/x-bittorrent"; + + /** Attachment's file name */ + protected static final String FILE_NAME = "; filename="; + + /** Attachment's file name as a byte array */ + private static final byte[] FILE_NAME_BYTES = + EncodingUtils.getAsciiBytes(FILE_NAME); + + /** Source of the file part. */ + private PartSource source; + + /** + * FilePart Constructor. + * + * @param name the name for this part + * @param partSource the source for this part + * @param contentType the content type for this part, if null the + * {@link #DEFAULT_CONTENT_TYPE default} is used + * @param charset the charset encoding for this part, if null the + * {@link #DEFAULT_CHARSET default} is used + */ + public BitCometFilePart(String name, PartSource partSource, String contentType, String charset) { + + super( + name, + contentType == null ? DEFAULT_CONTENT_TYPE : contentType, + charset == null ? "ISO-8859-1" : charset, + null + ); + + if (partSource == null) { + throw new IllegalArgumentException("Source may not be null"); + } + this.source = partSource; + } + + /** + * FilePart Constructor. + * + * @param name the name for this part + * @param partSource the source for this part + */ + public BitCometFilePart(String name, PartSource partSource) { + this(name, partSource, null, null); + } + + /** + * FilePart Constructor. + * + * @param name the name of the file part + * @param file the file to post + * + * @throws FileNotFoundException if the file is not a normal + * file or if it is not readable. + */ + public BitCometFilePart(String name, File file) + throws FileNotFoundException { + this(name, new FilePartSource(file), null, null); + } + + /** + * FilePart Constructor. + * + * @param name the name of the file part + * @param file the file to post + * @param contentType the content type for this part, if null the + * {@link #DEFAULT_CONTENT_TYPE default} is used + * @param charset the charset encoding for this part, if null the + * {@link #DEFAULT_CHARSET default} is used + * + * @throws FileNotFoundException if the file is not a normal + * file or if it is not readable. + */ + public BitCometFilePart(String name, File file, String contentType, String charset) + throws FileNotFoundException { + this(name, new FilePartSource(file), contentType, charset); + } + + /** + * FilePart Constructor. + * + * @param name the name of the file part + * @param fileName the file name + * @param file the file to post + * + * @throws FileNotFoundException if the file is not a normal + * file or if it is not readable. + */ + public BitCometFilePart(String name, String fileName, File file) + throws FileNotFoundException { + this(name, new FilePartSource(fileName, file), null, null); + } + + /** + * FilePart Constructor. + * + * @param name the name of the file part + * @param fileName the file name + * @param file the file to post + * @param contentType the content type for this part, if null the + * {@link #DEFAULT_CONTENT_TYPE default} is used + * @param charset the charset encoding for this part, if null the + * {@link #DEFAULT_CHARSET default} is used + * + * @throws FileNotFoundException if the file is not a normal + * file or if it is not readable. + */ + public BitCometFilePart(String name, String fileName, File file, String contentType, String charset) + throws FileNotFoundException { + this(name, new FilePartSource(fileName, file), contentType, charset); + } + + /** + * Write the disposition header to the output stream + * @param out The output stream + * @throws IOException If an IO problem occurs + * @see Part#sendDispositionHeader(OutputStream) + */ + @Override + protected void sendDispositionHeader(OutputStream out) + throws IOException { + super.sendDispositionHeader(out); + String filename = this.source.getFileName(); + if (filename != null) { + out.write(FILE_NAME_BYTES); + out.write(QUOTE_BYTES); + out.write(EncodingUtils.getAsciiBytes(filename)); + out.write(QUOTE_BYTES); + } + } + + /** + * Write the data in "source" to the specified stream. + * @param out The output stream. + * @throws IOException if an IO problem occurs. + * @see Part#sendData(OutputStream) + */ + @Override + protected void sendData(OutputStream out) throws IOException { + if (lengthOfData() == 0) { + + // this file contains no data, so there is nothing to send. + // we don't want to create a zero length buffer as this will + // cause an infinite loop when reading. + return; + } + + byte[] tmp = new byte[4096]; + InputStream instream = source.createInputStream(); + try { + int len; + while ((len = instream.read(tmp)) >= 0) { + out.write(tmp, 0, len); + } + } finally { + // we're done with the stream, close it + instream.close(); + } + } + + /** + * Returns the source of the file part. + * + * @return The source. + */ + protected PartSource getSource() { + return this.source; + } + + /** + * Return the length of the data. + * @return The length. + * @see Part#lengthOfData() + */ + @Override + protected long lengthOfData() { + return source.getLength(); + } + +} diff --git a/lib/src/com/android/internalcopy/http/multipart/Utf8StringPart.java b/lib/src/com/android/internalcopy/http/multipart/Utf8StringPart.java new file mode 100644 index 00000000..3a2f28ba --- /dev/null +++ b/lib/src/com/android/internalcopy/http/multipart/Utf8StringPart.java @@ -0,0 +1,43 @@ +package com.android.internalcopy.http.multipart; + +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.http.util.EncodingUtils; + +public class Utf8StringPart extends PartBase { + + /** Contents of this StringPart. */ + private byte[] content; + + /** The String value of this part. */ + private String value; + + public Utf8StringPart(String name, String value) { + super(name, null, null, null); + this.value = value; + } + + /** + * Gets the content in bytes. Bytes are lazily created to allow the charset to be changed + * after the part is created. + * + * @return the content in bytes + */ + private byte[] getContent() { + if (content == null) { + content = EncodingUtils.getBytes(value, "utf-8"); + } + return content; + } + + @Override + protected void sendData(OutputStream out) throws IOException { + out.write(getContent()); + } + + @Override + protected long lengthOfData() throws IOException { + return getContent().length; + } +} diff --git a/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java b/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java new file mode 100644 index 00000000..bc35ee80 --- /dev/null +++ b/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java @@ -0,0 +1,536 @@ +/* + * This file is part of Transdroid + * + * Transdroid 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 3 of the License, or + * (at your option) any later version. + * + * Transdroid 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 Transdroid. If not, see . + * + */ +package org.transdroid.daemon.BitComet; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.protocol.HTTP; +import org.transdroid.daemon.Daemon; +import org.transdroid.daemon.DaemonException; +import org.transdroid.daemon.DaemonSettings; +import org.transdroid.daemon.IDaemonAdapter; +import org.transdroid.daemon.Priority; +import org.transdroid.daemon.Torrent; +import org.transdroid.daemon.TorrentFile; +import org.transdroid.daemon.TorrentStatus; +import org.transdroid.daemon.DaemonException.ExceptionType; +import org.transdroid.daemon.task.AddByFileTask; +import org.transdroid.daemon.task.AddByUrlTask; +import org.transdroid.daemon.task.AddByMagnetUrlTask; +import org.transdroid.daemon.task.DaemonTask; +import org.transdroid.daemon.task.DaemonTaskFailureResult; +import org.transdroid.daemon.task.DaemonTaskResult; +import org.transdroid.daemon.task.DaemonTaskSuccessResult; +import org.transdroid.daemon.task.GetFileListTask; +import org.transdroid.daemon.task.GetFileListTaskSuccessResult; +import org.transdroid.daemon.task.RemoveTask; +import org.transdroid.daemon.task.RetrieveTask; +import org.transdroid.daemon.task.RetrieveTaskSuccessResult; +import org.transdroid.daemon.util.DLog; +import org.transdroid.daemon.util.HttpHelper; + +import com.android.internalcopy.http.multipart.Part; +import com.android.internalcopy.http.multipart.MultipartEntity; +import com.android.internalcopy.http.multipart.BitCometFilePart; +import com.android.internalcopy.http.multipart.Utf8StringPart; + +/** + * The daemon adapter for the BitComet torrent client. + * + * @author SeNS (sensboston) + * + */ +public class BitCometAdapter implements IDaemonAdapter { + + private static final String LOG_NAME = "BitComet daemon"; + + private DaemonSettings settings; + private DefaultHttpClient httpclient; + + public BitCometAdapter(DaemonSettings settings) { + this.settings = settings; + } + + @Override + public DaemonTaskResult executeTask(DaemonTask task) { + + try { + switch (task.getMethod()) { + case Retrieve: + + // Request all torrents from server + String result = makeRequest("/panel/task_list"); + return new RetrieveTaskSuccessResult((RetrieveTask) task, parseHttpTorrents(result), null); + + case GetFileList: + + // Request files listing for a specific torrent + String fhash = ((GetFileListTask)task).getTargetTorrent().getUniqueID(); + result = makeRequest("/panel/task_detail", new BasicNameValuePair("id", fhash), new BasicNameValuePair("show", "files")); + return new GetFileListTaskSuccessResult((GetFileListTask) task, parseHttpTorrentFiles(result, fhash)); + + case AddByFile: + + // Upload a local .torrent file + String ufile = ((AddByFileTask)task).getFile(); + makeFileUploadRequest("/panel/task_add_bt_result", ufile); + return new DaemonTaskSuccessResult(task); + + case AddByUrl: + + // Request to add a torrent by URL + String url = ((AddByUrlTask)task).getUrl(); + makeUploadUrlRequest("/panel/task_add_httpftp_result", url); + return new DaemonTaskSuccessResult(task); + + case AddByMagnetUrl: + + // Request to add a torrent by URL + String magnetUrl = ((AddByMagnetUrlTask)task).getUrl(); + makeUploadUrlRequest("/panel/task_add_httpftp_result", magnetUrl); + return new DaemonTaskSuccessResult(task); + + case Remove: + + // Remove a torrent + RemoveTask removeTask = (RemoveTask) task; + makeRequest("/panel/task_delete", new BasicNameValuePair("id", removeTask.getTargetTorrent().getUniqueID()), + new BasicNameValuePair("action", (removeTask.includingData()? "delete_all": "delete_task"))); + return new DaemonTaskSuccessResult(task); + + case Pause: + + // Pause a torrent + makeRequest("/panel/task_action", new BasicNameValuePair("id", task.getTargetTorrent().getUniqueID()), new BasicNameValuePair("action", "stop")); + return new DaemonTaskSuccessResult(task); + + case Resume: + + // Resume a torrent + makeRequest("/panel/task_action", new BasicNameValuePair("id", task.getTargetTorrent().getUniqueID()), new BasicNameValuePair("action", "start")); + return new DaemonTaskSuccessResult(task); + + default: + return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported, task.getMethod() + " is not supported by " + getType())); + } + } + catch (DaemonException e) { + return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ParsingFailed, e.toString())); + } + } + + /** + * Instantiates an HTTP client with proper credentials that can be used for all Buffalo NAS requests. + * @param connectionTimeout The connection timeout in milliseconds + * @throws DaemonException On conflicting or missing settings + */ + private void initialise(int connectionTimeout) throws DaemonException { + + httpclient = HttpHelper.createStandardHttpClient(settings, true); + } + + /** + * Build the URL of the http request from the user settings + * @return The URL to request + */ + private String buildWebUIUrl(String path) { + return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + path; + } + + private String makeRequest(String url, NameValuePair... params) throws DaemonException { + + try { + + // Initialize the HTTP client + if (httpclient == null) { + initialise(HttpHelper.DEFAULT_CONNECTION_TIMEOUT); + } + + // Add the parameters to the query string + boolean first = true; + for (NameValuePair param : params) { + if (first) { + url += "?"; + first = false; + } else { + url += "&"; + } + url += param.getName() + "=" + param.getValue(); + } + + // Make the request + HttpResponse response = httpclient.execute(new HttpGet(buildWebUIUrl(url))); + HttpEntity entity = response.getEntity(); + if (entity != null) { + + // Read JSON response + java.io.InputStream instream = entity.getContent(); + String result = HttpHelper.ConvertStreamToString(instream); + instream.close(); + + // Return raw result + return result; + } + + DLog.d(LOG_NAME, "Error: No entity in HTTP response"); + throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity object in response."); + + } catch (UnsupportedEncodingException e) { + throw new DaemonException(ExceptionType.ConnectionError, e.toString()); + } catch (Exception e) { + DLog.d(LOG_NAME, "Error: " + e.toString()); + throw new DaemonException(ExceptionType.ConnectionError, e.toString()); + } + + } + + private boolean makeFileUploadRequest(String path, String file) throws DaemonException { + + try { + + // Initialize the HTTP client + if (httpclient == null) { + initialise(HttpHelper.DEFAULT_CONNECTION_TIMEOUT); + } + + // Get default download file location first + HttpResponse response = httpclient.execute(new HttpGet(buildWebUIUrl("/panel/task_add_bt"))); + HttpEntity entity = response.getEntity(); + if (entity != null) { + + // Read BitComet response + java.io.InputStream instream = entity.getContent(); + String result = HttpHelper.ConvertStreamToString(instream); + instream.close(); + + int idx = result.indexOf("save_path' value='")+18; + String defaultPath = result.substring(idx, result.indexOf("'>", idx)); + + // Setup request using POST + HttpPost httppost = new HttpPost(buildWebUIUrl(path)); + File upload = new File(URI.create(file)); + Part[] parts = { new BitCometFilePart("torrent_file", upload), new Utf8StringPart("save_path", defaultPath) }; + httppost.setEntity(new MultipartEntity(parts, httppost.getParams())); + + // Make the request + response = httpclient.execute(httppost); + + entity = response.getEntity(); + if (entity != null) { + // Check BitComet response + instream = entity.getContent(); + result = HttpHelper.ConvertStreamToString(instream); + instream.close(); + if (result.indexOf("failed!") > 0) throw new Exception("Adding torrent file failed"); + } + + return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK; + } + return false; + + } catch (FileNotFoundException e) { + throw new DaemonException(ExceptionType.FileAccessError, e.toString()); + } catch (Exception e) { + DLog.d(LOG_NAME, "Error: " + e.toString()); + throw new DaemonException(ExceptionType.ConnectionError, e.toString()); + } + } + + private boolean makeUploadUrlRequest(String path, String url) throws DaemonException { + + try { + + // Initialize the HTTP client + if (httpclient == null) { + initialise(HttpHelper.DEFAULT_CONNECTION_TIMEOUT); + } + + // Get default download file location first + HttpResponse response = httpclient.execute(new HttpGet(buildWebUIUrl("/panel/task_add_httpftp"))); + HttpEntity entity = response.getEntity(); + if (entity != null) { + + // Read BitComet response + java.io.InputStream instream = entity.getContent(); + String result = HttpHelper.ConvertStreamToString(instream); + instream.close(); + + int idx = result.indexOf("save_path' value='")+18; + String defaultPath = result.substring(idx, result.indexOf("'>", idx)); + + // Setup form fields and post request + HttpPost httppost = new HttpPost(buildWebUIUrl(path)); + + List params = new ArrayList(); + params.add(new BasicNameValuePair("url", url)); + params.add(new BasicNameValuePair("save_path", defaultPath)); + params.add(new BasicNameValuePair("connection", "5")); + params.add(new BasicNameValuePair("ReferPage", "")); + params.add(new BasicNameValuePair("textSpeedLimit", "0")); + httppost.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8)); + + // Make the request + response = httpclient.execute(httppost); + + entity = response.getEntity(); + if (entity != null) { + // Check BitComet response + instream = entity.getContent(); + result = HttpHelper.ConvertStreamToString(instream); + instream.close(); + if (result.indexOf("failed!") > 0) { + throw new Exception("Adding URL failed"); + } + } + + return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK; + } + return false; + } catch (Exception e) { + DLog.d(LOG_NAME, "Error: " + e.toString()); + throw new DaemonException(ExceptionType.ConnectionError, e.toString()); + } + } + + /** + * Parse BitComet HTML page (http response) + * @param response + * @return + * @throws DaemonException + */ + private ArrayList parseHttpTorrents(String response) throws DaemonException { + + ArrayList torrents = new ArrayList(); + + try { + + String[] parts = response.substring(response.indexOf("")).replaceAll("", "").replaceAll("", "").replaceAll("\n", "").split(""); + + for (int i=2; i", "")+1, name.indexOf("<")); + + TorrentStatus status = parseStatus(subParts[3]); + String percenDoneStr = subParts[6]; + String downloadRateStr = subParts[7]; + String uploadRateStr = subParts[8]; + + long size = convertSize(subParts[5]); + float percentDone = Float.parseFloat(percenDoneStr.substring(0, percenDoneStr.indexOf("%"))); + long sizeDone = (long) (size * percentDone / 100 ); + + int rateUp = 1000 * Integer.parseInt(uploadRateStr.substring(0, uploadRateStr.indexOf("kB/s"))); + int rateDown = 1000 * Integer.parseInt(downloadRateStr.substring(0, downloadRateStr.indexOf("kB/s"))); + + // Unfortunately, there is no info for above values providing by BitComet now, + // so we may only send additional request for that + int leechers = 0; + int seeders = 0; + int knownLeechers = 0; + int knownSeeders = 0; + int distributed_copies = 0; + long sizeUp = 0; + String comment = ""; + Date dateAdded = new Date(); + + // Comment code below to speedup torrent listing + // P.S. feature request to extend torrents info is already sent to the BitComet developers + //* + try { + // Lets make summary request and parse details + String summary = makeRequest("/panel/task_detail", new BasicNameValuePair("id", ""+(i-2)), new BasicNameValuePair("show", "summary")); + + String[] sumParts = summary.substring(summary.indexOf("
Value
")).split(""); + comment = sumParts[7].substring(sumParts[7].indexOf("")+4, sumParts[7].indexOf("")); + + // Indexes for date and uploaded size + int idx = 9; + int sizeIdx = 12; + + if (status == TorrentStatus.Downloading) { + seeders = Integer.parseInt(sumParts[9].substring(sumParts[9].indexOf("Seeds:")+6, sumParts[9].indexOf("(Max possible"))); + leechers = Integer.parseInt(sumParts[9].substring(sumParts[9].indexOf("Peers:")+6, sumParts[9].lastIndexOf("(Max possible"))); + knownSeeders = Integer.parseInt(sumParts[9].substring(sumParts[9].indexOf("(Max possible:")+14, sumParts[9].indexOf(")"))); + knownLeechers = Integer.parseInt(sumParts[9].substring(sumParts[9].lastIndexOf("(Max possible:")+14, sumParts[9].lastIndexOf(")"))); + idx = 13; + sizeIdx = 16; + } + + DateFormat df = new SimpleDateFormat("yyyy-mm-dd kk:mm:ss"); + dateAdded = df.parse(sumParts[idx].substring(sumParts[idx].indexOf("")+4, sumParts[idx].indexOf(""))); + //sizeDone = convertSize(sumParts[sizeIdx].substring(sumParts[sizeIdx].indexOf("")+4, sumParts[sizeIdx].indexOf(" ("))); + sizeUp = convertSize(sumParts[sizeIdx+1].substring(sumParts[sizeIdx+1].indexOf("")+4, sumParts[sizeIdx+1].indexOf(" ("))); + } + catch (Exception e) {} + //* + + // Add the parsed torrent to the list + torrents.add(new Torrent( + (long)i-2, + null, + name, + status, + null, + rateDown, + rateUp, + leechers, + seeders, + knownLeechers, + knownSeeders, + (rateDown == 0? -1: (int) ((size - sizeDone) / rateDown)), + sizeDone, + sizeUp, + size, + percentDone / 100, + distributed_copies, + comment, + dateAdded, + null)); + } + } + } + catch (Exception e) { + throw new DaemonException(ExceptionType.UnexpectedResponse, "Invalid BitComet HTTP response."); + } + + return torrents; + } + + /** + * Parse BitComet HTML page (http response) + * @param response + * @return + * @throws DaemonException + */ + private ArrayList parseHttpTorrentFiles(String response, String hash) throws DaemonException { + + // Parse response + ArrayList torrentfiles = new ArrayList(); + + try { + + String[] files = response.substring(response.indexOf("Operation Method")+27, response.lastIndexOf("")).replaceAll("", "").replaceAll("", "").split(""); + + for (int i = 1; i < files.length; i++) { + + String[] fileDetails = files[i].replace(">","").split("