I've created diff for that (against svn revision 10530, as per `svn diff'), altogether 349 lines of code.
To support the feature a new rpc call is exposed: 'folders-get'.
Input parameter: 'path', string, required (should be a full pathname).
Output:
'folders' -- array of strings (list of folders existing in path to which transmission user has access).
'pathDelim' -- string, server path delimiter (/ or \).
'folderName' -- string, cleaned version of input path.
'parentFolder' -- string, contains a pathname of folder's parent (if path is root parameter is absent on output).
The change has been tested on Linux and MacOS both as server and client, Firefox and Safari. I have no Windows, but rpc should work (except for manual selection of alternative drive letter).
I would appreciate if diff will be integrated.
Screenshoots:
http://lh3.ggpht.com/_1LXhCJBcaMk/S9ihc ... 7%20AM.png
http://lh3.ggpht.com/_1LXhCJBcaMk/S9ihc ... 1%20AM.png
Code: Select all
Index: libtransmission/utils.c
===================================================================
--- libtransmission/utils.c (revision 10530)
+++ libtransmission/utils.c (working copy)
@@ -657,7 +657,10 @@
const size_t elementLen = strlen( element );
memcpy( pch, element, elementLen );
pch += elementLen;
- *pch++ = TR_PATH_DELIMITER;
+ if( elementLen > 1 && element[elementLen-1] != TR_PATH_DELIMITER )
+ *pch++ = TR_PATH_DELIMITER;
+ else
+ bufLen--;
element = (const char*) va_arg( vl, const char* );
}
va_end( vl );
Index: libtransmission/rpcimpl.c
===================================================================
--- libtransmission/rpcimpl.c (revision 10530)
+++ libtransmission/rpcimpl.c (working copy)
@@ -16,8 +16,9 @@
#include <stdlib.h> /* strtol */
#include <string.h> /* strcmp */
#include <unistd.h> /* unlink */
-
+#include <dirent.h> /* opendir */
#include <event.h> /* evbuffer */
+#include <sys/stat.h>
#include "transmission.h"
#include "bencode.h"
@@ -30,6 +31,7 @@
#include "utils.h"
#include "version.h"
#include "web.h"
+#include "platform.h"
#define RPC_VERSION 9
#define RPC_VERSION_MIN 1
@@ -1084,9 +1086,18 @@
/* set the optional arguments */
- if( tr_bencDictFindStr( args_in, TR_PREFS_KEY_DOWNLOAD_DIR, &str ) )
- tr_ctorSetDownloadDir( ctor, TR_FORCE, str );
+ if( tr_bencDictFindStr( args_in, TR_PREFS_KEY_DOWNLOAD_DIR, &str ) ) {
+ char * path;
+ while ( isspace(*str) ) str++;
+ if( *str != TR_PATH_DELIMITER ) /* file path is not fully qualified */
+ path = tr_buildPath( tr_getDefaultDownloadDir(), str, NULL );
+ else /* otherwise still call tr_buildPath to strip possible unwanted training '/' */
+ path = tr_buildPath( str, NULL );
+ tr_ctorSetDownloadDir( ctor, TR_FORCE, path );
+ tr_free( path );
+ }
+
if( tr_bencDictFindBool( args_in, "paused", &boolVal ) )
tr_ctorSetPaused( ctor, TR_FORCE, boolVal );
@@ -1351,6 +1362,126 @@
return NULL;
}
+static const char*
+addFolders( const char * path,
+ tr_benc * folders )
+{
+ DIR * odir;
+ struct dirent *d;
+
+ odir = opendir ( path );
+ if ( odir == NULL ) return tr_strerror(errno);
+ for( d = readdir( odir ); d != NULL; d = readdir( odir ) )
+ {
+ if( d->d_name && strcmp(d->d_name,".") != 0 && strcmp(d->d_name,"..") != 0 )
+ {
+ char * buf = tr_buildPath( path, d->d_name, NULL );
+ struct stat sb;
+
+ if ( !stat( buf, &sb ) )
+ {
+ if( S_ISDIR( sb.st_mode ) && access( buf, R_OK|X_OK) == 0 )
+ tr_bencListAddStr( folders, d->d_name );
+ }
+ tr_free(buf);
+ }
+ }
+ closedir( odir );
+ return NULL;
+}
+
+#ifdef WIN32
+# define IS_PATH_DELIM(c) (strchr("\\/", (c)))
+# define ROOT_PATH_LEN 3
+#else
+# define IS_PATH_DELIM(c) ((c) == TR_PATH_DELIMITER)
+# define ROOT_PATH_LEN 1
+#endif
+
+static const char*
+foldersGet( tr_session * session UNUSED,
+ tr_benc * args_in,
+ tr_benc * args_out,
+ struct tr_rpc_idle_data * idle_data UNUSED )
+{
+ tr_benc * d = args_out;
+ const char * req_path = NULL;
+ const char * ret;
+ char * path;
+ size_t len;
+ int up_level = 0;
+
+ assert( idle_data == NULL );
+
+ tr_bencDictFindStr( args_in, "path", &req_path );
+ if( !req_path )
+ return "no path specified";
+
+ path = tr_strdup(req_path);
+ if( !path )
+ return strerror(errno);
+
+ len = strlen(path);
+
+ if( len < ROOT_PATH_LEN ||
+#ifdef WIN32
+ path[1] != ':' ||
+#endif
+ !IS_PATH_DELIM(path[ROOT_PATH_LEN-1]) )
+ {
+ tr_free(path);
+ path = tr_strdup(tr_getDefaultDownloadDir());
+ if( !path )
+ return strerror(errno);
+ len = strlen(path);
+ }
+
+ /* use of POSIX realpath() would easy all of that, but its availablity is unclear */
+ for(;;) {
+ /* strip possible multiple path delimiters at the end of pathname, except for root folder */
+ while ( len > ROOT_PATH_LEN && IS_PATH_DELIM(path[len-1]) )
+ path[--len] = '\0';
+
+ /* strip possible . and .. at the end of path */
+ if ( len > ROOT_PATH_LEN && path[len-1] == '.' && IS_PATH_DELIM(path[len-2]) ) /* trailing /. */
+ {
+ path[--len] = '\0';
+ continue;
+ }
+ else if ( len > ROOT_PATH_LEN + 1 && path[len-1] == '.' && path[len-2] == '.' && IS_PATH_DELIM(path[len-3]) ) /* trailing /.. */
+ {
+ len -= 2; /* strip .. */
+ if( len > ROOT_PATH_LEN ) len--; /* and path delimiter if not root */
+ path[len] = '\0';
+ up_level++;
+ continue;
+ }
+ else if ( up_level > 0 )
+ {
+ up_level--;
+ while( len > ROOT_PATH_LEN && !IS_PATH_DELIM(path[len-1]) ) len--; /* strip one parent folder name */
+ path[len] = '\0';
+ continue;
+ }
+ break;
+ }
+
+ tr_bencDictAddStr ( d, "pathDelim", TR_PATH_DELIMITER_STR );
+ tr_bencDictAddStr ( d, "folderName", path );
+ ret = addFolders( path, tr_bencDictAddList( d, "folders", 1 ) );
+ if( len > ROOT_PATH_LEN )
+ {
+ path = tr_dirname(path);
+ tr_bencDictAddStr ( d, "parentFolder", path );
+ tr_free(path);
+ }
+
+ return ret;
+}
+
+#undef ROOT_PATH_LEN
+#undef IS_PATH_DELIM
+
/***
****
***/
@@ -1378,7 +1509,8 @@
{ "torrent-start", TRUE, torrentStart },
{ "torrent-stop", TRUE, torrentStop },
{ "torrent-verify", TRUE, torrentVerify },
- { "torrent-reannounce", TRUE, torrentReannounce }
+ { "torrent-reannounce", TRUE, torrentReannounce },
+ { "folders-get", TRUE, foldersGet }
};
static void
Index: libtransmission/rpc-server.c
===================================================================
--- libtransmission/rpc-server.c (revision 10530)
+++ libtransmission/rpc-server.c (working copy)
@@ -212,8 +212,22 @@
tr_ptrArray parts = TR_PTR_ARRAY_INIT;
const char * query = strchr( req->uri, '?' );
- const tr_bool paused = query && strstr( query + 1, "paused=true" );
+ tr_bool paused;
+ char * download_dir = NULL;
+ while( query != NULL ) {
+ query++;
+ if( strncmp(query, "paused=", 7) == 0 ) {
+ query += 7;
+ paused = ( strncmp(query, "true", 4) == 0 );
+ } else if( download_dir == NULL && strncmp(query, "download-dir=", 13) == 0 ) {
+ query += 13;
+ download_dir = strchr( query, '?' );
+ download_dir = tr_strndup( query, download_dir ? download_dir - query : (int)strlen(query) );
+ }
+ query = strchr( query, '?' );
+ }
+
extract_parts_from_multipart( req->input_headers, req->input_buffer, &parts );
n = tr_ptrArraySize( &parts );
@@ -254,6 +268,8 @@
b64 = tr_base64_encode( body, body_len, NULL );
tr_bencDictAddStr( args, "metainfo", b64 );
tr_bencDictAddBool( args, "paused", paused );
+ if( download_dir != NULL )
+ tr_bencDictAddStr( args, "download-dir", download_dir );
tr_bencToBuf( &top, TR_FMT_JSON_LEAN, json );
tr_rpc_request_exec_json( server->session,
EVBUFFER_DATA( json ),
@@ -267,6 +283,7 @@
}
tr_ptrArrayDestruct( &parts, (PtrArrayForeachFunc)tr_mimepart_free );
+ tr_free( download_dir );
/* use xml here because json responses to file uploads is trouble.
* see http://www.malsup.com/jquery/form/#sample7 for details */
Index: web/javascript/transmission.js
===================================================================
--- web/javascript/transmission.js (revision 10530)
+++ web/javascript/transmission.js (working copy)
@@ -58,6 +58,9 @@
$('#turtle_button').bind('click', function(e){ tr.toggleTurtleClicked(e); return false; });
$('#prefs_tab_general_tab').click(function(e){ changeTab(this, 'prefs_tab_general') });
$('#prefs_tab_speed_tab').click(function(e){ changeTab(this, 'prefs_tab_speed') });
+ $('#torrent_data_dir_browse').bind('click', function(e){ tr.openForlderClicked(e); });
+ $('#folder_confirm_button').bind('click', function(e){ tr.confirmFolderClicked(e); return false;});
+ $('#folder_cancel_button').bind('click', function(e){ tr.cancelFolderClicked(e); return false;});
if (iPhone) {
$('#inspector_close').bind('click', function(e){ tr.hideInspector(); });
@@ -117,7 +120,7 @@
// Setup the prefs gui
this.initializeSettings( );
-
+
// Get preferences & torrents from the daemon
var tr = this;
var async = false;
@@ -623,6 +626,30 @@
this.hideUploadDialog( );
},
+ confirmFolderClicked: function(event) {
+ var tr = this;
+ if( tr.isButtonEnabled( event ) ) {
+ var o = $('#folder_list').find('.selected');
+ if( o.length != 0 ) $('#torrent_data_dir').attr('value', o.find('a').attr('rel'));
+ $('#select_folder_container').hide();
+ }
+ tr.updateButtonStates();
+ },
+
+ cancelFolderClicked: function(event) {
+ $('#select_folder_container').hide();
+ },
+
+ openForlderClicked: function( event ) {
+ var tr = this;
+ if( tr.isButtonEnabled( event ) ) {
+ $('#folder_list').selectFolder({ root: $('#torrent_data_dir').val() }, tr.remote);
+ $('#select_folder_container').show();
+ $('#folder_list').scrollTop(0);
+ }
+ tr.updateButtonStates();
+ },
+
cancelPrefsClicked: function(event) {
this.hidePrefsDialog( );
},
@@ -1637,6 +1664,7 @@
if (! confirmed) {
$('input#torrent_upload_file').attr('value', '');
$('input#torrent_upload_url').attr('value', '');
+ $('input#torrent_data_dir').attr('value', $('#prefs_form #download_location')[0].value);
$('input#torrent_auto_start').attr('checked', $('#prefs_form #auto_start')[0].checked);
$('#upload_container').show();
if (!iPhone && Safari3) {
@@ -1649,9 +1677,9 @@
var args = { };
var paused = !$('#torrent_auto_start').is(':checked');
if ('' != $('#torrent_upload_url').val()) {
- tr.remote.addTorrentByUrl($('#torrent_upload_url').val(), { paused: paused });
+ tr.remote.addTorrentByUrl($('#torrent_upload_url').val(), { paused: paused }, $('#torrent_data_dir').val());
} else {
- args.url = '/transmission/upload?paused=' + paused;
+ args.url = '/transmission/upload?paused=' + paused + '?download-dir=' + $('#torrent_data_dir').val();
args.type = 'POST';
args.data = { 'X-Transmission-Session-Id' : tr.remote._token };
args.dataType = 'xml';
@@ -1865,3 +1893,48 @@
}
}
};
+
+(function($){
+
+ $.extend($.fn, {
+ selectFolder: function(o, remote) {
+ if( !o ) var o = {};
+ if( o.root == undefined ) o.root = '/';
+ else o.root = o.root.replace(/^\s+|\s+$/g,"");
+ if( o.loadMessage == undefined ) o.loadMessage = 'Loading...';
+
+ function showFolder(c, t) {
+ $(c).html('<ul><li>' + o.loadMessage + '</li></ul>');
+
+ remote.getFolders( t, function(data){
+ var args = data.arguments;
+ var body = '<ul>';
+ body = body + '<li class="this_link selected"><a href="#" rel="'+args.folderName+'">'+args.folderName+'</a></li>';
+ if( args.parentFolder != undefined ) body = body + '<li class="parent_link"><a href="#" rel="'+args.parentFolder+'">..</a></li>';
+ jQuery.each( args.folders.sort(), function() {
+ body = body + '<li><a href="#" rel="' + args.folderName;
+ if( args.parentFolder != undefined ) body = body + args.pathDelim;
+ body = body + this + '">' + this + '</a></li>';
+ });
+ body = body + '</ul>';
+ $(c).html(body);
+ $(c).scrollTop(0);
+ $(c).find('li a').bind('dblclick', function() {
+ showFolder( $(c), $(this).attr('rel') );
+ return false;
+ });
+ $(c).find('li a').bind('click', function() {
+ $(c).parent().find('.selected').removeClass('selected');
+ $(this).parent().addClass('selected');
+ return false;
+ });
+ });
+
+ }
+
+ showFolder( $(this), o.root );
+ }
+ });
+
+})(jQuery);
+
Index: web/javascript/transmission.remote.js
===================================================================
--- web/javascript/transmission.remote.js (revision 10530)
+++ web/javascript/transmission.remote.js (working copy)
@@ -227,12 +227,13 @@
verifyTorrents: function( torrent_ids, callback ) {
this.sendTorrentActionRequests( 'torrent-verify', torrent_ids, callback );
},
- addTorrentByUrl: function( url, options ) {
+ addTorrentByUrl: function( url, options, download_dir ) {
var remote = this;
var o = {
method: 'torrent-add',
arguments: {
paused: (options.paused),
+ 'download-dir': download_dir,
filename: url
}
};
@@ -256,5 +257,14 @@
},
filesDeselectAll: function( torrent_ids, files, callback ) {
this.sendTorrentSetRequests( 'torrent-set', torrent_ids, { 'files-unwanted': files }, callback );
+ },
+ getFolders: function(path, callback) {
+ var o = {
+ method: 'folders-get',
+ arguments: {
+ path: path
+ }
+ };
+ this.sendRequest( o, callback, false );
}
};
Index: web/index.html
===================================================================
--- web/index.html (revision 10530)
+++ web/index.html (working copy)
@@ -385,7 +385,12 @@
<label for="torrent_upload_file">Please select a torrent file to upload:</label>
<input type="file" name="torrent_files[]" id="torrent_upload_file" multiple="multiple" />
<label for="torrent_upload_url">Or enter a URL:</label>
- <input type="text" id="torrent_upload_url"/>
+ <input type="text" id="torrent_upload_url" />
+ <div class="dialog_input_group">
+ <label for="torrent_data_dir">Download directory:</label>
+ <input type="text" id="torrent_data_dir" />
+ <input type="button" id="torrent_data_dir_browse" value="Browse..." />
+ </div>
<input type="checkbox" id="torrent_auto_start" />
<label for="torrent_auto_start" id="auto_start_label">Start when added</label>
</div>
@@ -394,6 +399,19 @@
</form>
</div>
</div>
+
+ <div class="dialog_container" id="select_folder_container" style="display:none;">
+ <div class="dialog_top_bar"></div>
+ <div class="dialog_window">
+ <h2 class="dialog_heading">Select target folder</h2>
+ <form action="#" method="post" id="select_folder_form" target="select_folder_frame">
+ <div id="folder_list"></div>
+ <a href="#select" id="folder_confirm_button">Select</a>
+ <a href="#cancel" id="folder_cancel_button">Cancel</a>
+ </form>
+ </div>
+ </div>
+
<div class="torrent_footer">
<div id="disk_space_container"></div>
<ul id="settings_menu">
@@ -480,5 +498,7 @@
</div>
<iframe name="torrent_upload_frame" id="torrent_upload_frame" src="about:blank" ></iframe>
+ <iframe name="select_folder_frame" id="select_folder_frame" src="about:blank" ></iframe>
+
</body>
</html>
Index: web/stylesheets/common.css
===================================================================
--- web/stylesheets/common.css (revision 10530)
+++ web/stylesheets/common.css (working copy)
@@ -970,6 +970,26 @@
display: inline;
}
+div#upload_container div.dialog_window div.dialog_message div.dialog_input_group {
+ width: 245px;
+ margin: 3px 0 0 0;
+ display: table-row;
+}
+
+div#upload_container div.dialog_window div.dialog_message div.dialog_input_group input[type=text] {
+ width: 161px;
+ padding: 0px;
+ display: inline;
+}
+
+div#upload_container div.dialog_window div.dialog_message div.dialog_input_group input[type=button] {
+ width: 80px;
+ padding: 0px;
+ margin-right: 0px;
+ margin-left: 4px;
+ display: inline;
+}
+
div.dialog_container div.dialog_window form {
margin: 0;
padding: 0px;
@@ -987,6 +1007,62 @@
margin: 0;
}
+div#select_folder_container div.dialog_window {
+ height: 310px;
+}
+
+div#select_folder_container div.dialog_window h2.dialog_heading {
+ display: block;
+ float: none;
+ margin-top: 20px;
+ margin-left: 20px;
+}
+
+div#select_folder_container div.dialog_window div#folder_list {
+ display: block;
+ height: 210px;
+ margin-left: 0px;
+ margin-right: 5px;
+ overflow: auto;
+}
+
+div#select_folder_container div.dialog_window ul li {
+ list-style: none;
+ padding: 0px;
+ padding-left: 0px;
+ margin: 0px;
+ white-space: nowrap;
+ width: 100%;
+ border: none;
+}
+
+div#select_folder_container div.dialog_window ul li a {
+ float: none;
+ text-align: left;
+ font-weight: normal;
+ text-decoration: none;
+ border: none;
+ padding: 0;
+ width: 100%;
+ -webkit-appearance: none;
+}
+
+div#select_folder_container div.dialog_window ul li.selected a {
+ background: #BDF;
+}
+
+iframe#select_folder_frame {
+ display: block;
+ position: absolute;
+ top: -1000px;
+ left: -1000px;
+ width: 0px;
+ height: 0px;
+ border: none;
+ padding: 0;
+ margin: 0;
+}
+
div#prefs_container label {
display: block;
margin: 0 0 0 2px;