Merge pull request #176 from hunbernd/feature/webui-download-enhancements

Webui download and media streaming enhancements
This commit is contained in:
electron128 2015-11-08 10:23:45 +01:00
commit 0a21d92aca
8 changed files with 220 additions and 53 deletions

View File

@ -7,6 +7,7 @@
#include <algorithm> #include <algorithm>
#include <util/rsdir.h> #include <util/rsdir.h>
#include "util/ContentTypes.h"
// for filestreamer // for filestreamer
#include <retroshare/rsfiles.h> #include <retroshare/rsfiles.h>
@ -274,12 +275,21 @@ public:
{ {
sendMessage(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, "Error: rsFiles is null. Retroshare is probably not yet started."); sendMessage(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, "Error: rsFiles is null. Retroshare is probably not yet started.");
return MHD_YES; return MHD_YES;
} }
if(url[0] == 0 || (mHash=RsFileHash(url+strlen(FILESTREAMER_ENTRY_PATH))).isNull()) std::string urls(url);
{ urls = urls.substr(strlen(FILESTREAMER_ENTRY_PATH));
sendMessage(connection, MHD_HTTP_NOT_FOUND, "Error: URL is not a valid file hash"); size_t perpos = urls.find('/');
return MHD_YES; if(perpos == std::string::npos){
} mHash = RsFileHash(urls);
}else{
mHash = RsFileHash(urls.substr(0, perpos));
}
if(urls.empty() || mHash.isNull())
{
sendMessage(connection, MHD_HTTP_NOT_FOUND, "Error: URL is not a valid file hash");
return MHD_YES;
}
FileInfo info; FileInfo info;
std::list<RsFileHash> dls; std::list<RsFileHash> dls;
rsFiles->FileDownloads(dls); rsFiles->FileDownloads(dls);
@ -293,8 +303,13 @@ public:
struct MHD_Response* resp = MHD_create_response_from_callback( struct MHD_Response* resp = MHD_create_response_from_callback(
mSize, 1024*1024, &contentReadercallback, this, NULL); mSize, 1024*1024, &contentReadercallback, this, NULL);
// only mp3 at the moment // get content-type from extension
MHD_add_response_header(resp, "Content-Type", "audio/mpeg3"); std::string ext = "";
unsigned int i = info.fname.rfind('.');
if(i != std::string::npos)
ext = info.fname.substr(i+1);
MHD_add_response_header(resp, "Content-Type", ContentTypes::cTypeFromExt(ext).c_str());
secure_queue_response(connection, MHD_HTTP_OK, resp); secure_queue_response(connection, MHD_HTTP_OK, resp);
MHD_destroy_response(resp); MHD_destroy_response(resp);
return MHD_YES; return MHD_YES;
@ -635,27 +650,10 @@ int ApiServerMHD::accessHandlerCallback(MHD_Connection *connection,
{ {
extension = filename[i] + extension; extension = filename[i] + extension;
i--; i--;
} };
const char* type = 0;
if(extension == "html")
type = "text/html";
else if(extension == "css")
type = "text/css";
else if(extension == "js")
type = "text/javascript";
else if(extension == "jsx") // react.js jsx files
type = "text/jsx";
else if(extension == "png")
type = "image/png";
else if(extension == "jpg" || extension == "jpeg")
type = "image/jpeg";
else if(extension == "gif")
type = "image/gif";
else
type = "application/octet-stream";
struct MHD_Response* resp = MHD_create_response_from_fd(s.st_size, fd); struct MHD_Response* resp = MHD_create_response_from_fd(s.st_size, fd);
MHD_add_response_header(resp, "Content-Type", type); MHD_add_response_header(resp, "Content-Type", ContentTypes::cTypeFromExt(extension).c_str());
secure_queue_response(connection, MHD_HTTP_OK, resp); secure_queue_response(connection, MHD_HTTP_OK, resp);
MHD_destroy_response(resp); MHD_destroy_response(resp);
return MHD_YES; return MHD_YES;

View File

@ -59,7 +59,8 @@ SOURCES += \
api/GetPluginInterfaces.cpp \ api/GetPluginInterfaces.cpp \
api/ChatHandler.cpp \ api/ChatHandler.cpp \
api/LivereloadHandler.cpp \ api/LivereloadHandler.cpp \
api/TmpBlobStore.cpp api/TmpBlobStore.cpp \
util/ContentTypes.cpp
HEADERS += \ HEADERS += \
api/ApiServer.h \ api/ApiServer.h \
@ -82,4 +83,5 @@ HEADERS += \
api/GetPluginInterfaces.h \ api/GetPluginInterfaces.h \
api/ChatHandler.h \ api/ChatHandler.h \
api/LivereloadHandler.h \ api/LivereloadHandler.h \
api/TmpBlobStore.h api/TmpBlobStore.h \
util/ContentTypes.h

View File

@ -0,0 +1,88 @@
#include "ContentTypes.h"
#include <fstream>
#include <cctype>
#include <algorithm>
RsMutex ContentTypes::ctmtx = RsMutex("CTMTX");
std::map<std::string, std::string> ContentTypes::cache;
bool ContentTypes::inited = false;
#ifdef WINDOWS_SYS
//Next to the executable
const char* ContentTypes::filename = ".\\mime.types";
#else
const char* ContentTypes::filename = "/etc/mime.types";
#endif
std::string ContentTypes::cTypeFromExt(const std::string &extension)
{
if(extension.empty())
return DEFAULTCT;
RsStackMutex mtx(ctmtx);
if(!inited)
addBasic();
std::string extension2(extension); //lower case
std::transform(extension2.begin(), extension2.end(), extension2.begin(),::tolower);
//looking into the cache
std::map<std::string,std::string>::iterator it;
it = cache.find(extension2);
if (it != cache.end())
{
std::cout << "Mime " + it->second + " for extension ." + extension2 + " was found in cache" << std::endl;
return it->second;
}
//looking into mime.types
std::string line;
std::string ext;
std::ifstream file(filename);
while(getline(file, line))
{
if(line.empty() || line[0] == '#') continue;
unsigned int i = line.find_first_of("\t ");
unsigned int j;
while(i != std::string::npos) //tokenize
{
j = i;
i = line.find_first_of("\t ", i+1);
if(i == std::string::npos)
ext = line.substr(j+1);
else
ext = line.substr(j+1, i-j-1);
if(extension2 == ext)
{
std::string mime = line.substr(0, line.find_first_of("\t "));
cache[extension2] = mime;
std::cout << "Mime " + mime + " for extension ." + extension2 + " was found in mime.types" << std::endl;
return mime;
}
}
}
//nothing found
std::cout << "Mime for " + extension2 + " was not found in " + filename + " falling back to " << DEFAULTCT << std::endl;
cache[extension2] = DEFAULTCT;
return DEFAULTCT;
}
//Add some basic content-types before first use.
//It keeps webui usable in the case of mime.types file not exists.
void ContentTypes::addBasic()
{
inited = true;
cache["html"] = "text/html";
cache["css"] = "text/css";
cache["js"] = "text/javascript";
cache["jsx"] = "text/jsx";
cache["png"] = "image/png";
cache["jpg"] = "image/jpeg";
cache["jpeg"] = "image/jpeg";
cache["gif"] = "image/gif";
}

View File

@ -0,0 +1,23 @@
#ifndef CONTENTTYPES_H
#define CONTENTTYPES_H
#include <util/rsthreads.h>
#include <map>
#include <string>
#define DEFAULTCT "application/octet-stream"
class ContentTypes
{
public:
static std::string cTypeFromExt(const std::string& extension);
private:
static std::map<std::string, std::string> cache;
static RsMutex ctmtx;
static const char* filename;
static bool inited;
static void addBasic();
};
#endif // CONTENTTYPES_H

View File

@ -66,6 +66,10 @@ td{
background-color: midnightblue; background-color: midnightblue;
} }
.filelink{
color: inherit;
}
input, textarea{ input, textarea{
color: lime; color: lime;
font-family: monospace; font-family: monospace;

View File

@ -4,7 +4,15 @@ RS.start();
var api_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/api/v2/"; var api_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/api/v2/";
var filestreamer_url = window.location.protocol + "//" +window.location.hostname + ":" + window.location.port + "/fstream/"; var filestreamer_url = window.location.protocol + "//" +window.location.hostname + ":" + window.location.port + "/fstream/";
var upload_url = window.location.protocol + "//" +window.location.hostname + ":" + window.location.port + "/upload/"; var upload_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/upload/";
var extensions = {
mp3: "audio",
ogg: "audio",
wav: "audio",
webm: "video",
mp4: "video"
};
// livereload // livereload
function start_live_reload() function start_live_reload()
@ -558,7 +566,8 @@ var DownloadsWidget = React.createClass({
widget.emit("play_file", {name: file.name, hash: file.hash}) widget.emit("play_file", {name: file.name, hash: file.hash})
}; };
var playBtn = <div></div>; var playBtn = <div></div>;
if(file.name.slice(-3) === "mp3") var splits = file.name.split(".");
if(splits[splits.length-1].toLowerCase() in extensions)
playBtn = <div className="btn" onClick={playFn}>play</div>; playBtn = <div className="btn" onClick={playFn}>play</div>;
var ctrlBtn = <div></div>; var ctrlBtn = <div></div>;
@ -571,7 +580,7 @@ var DownloadsWidget = React.createClass({
ctrlBtn = <div className="btn" onClick={pauseFn}>pause</div>; ctrlBtn = <div className="btn" onClick={pauseFn}>pause</div>;
} }
return(<tr> return(<tr>
<td>{this.props.data.name}</td> <td><a className="filelink" target="_blank" href={filestreamer_url + this.props.data.hash + "/" + encodeURIComponent(this.props.data.name)}>{this.props.data.name}</a></td>
<td>{makeFriendlyUnit(this.props.data.size)}</td> <td>{makeFriendlyUnit(this.props.data.size)}</td>
<td><ProgressBar progress={this.props.data.transfered / this.props.data.size}/></td> <td><ProgressBar progress={this.props.data.transfered / this.props.data.size}/></td>
<td>{makeFriendlyUnit(this.props.data.transfer_rate*1e3)}/s</td> <td>{makeFriendlyUnit(this.props.data.transfer_rate*1e3)}/s</td>
@ -673,7 +682,7 @@ var SearchWidget = React.createClass({
}, },
}); });
var AudioPlayerWidget = React.createClass({ var MediaPlayerWidget = React.createClass({
mixins: [SignalSlotMixin], mixins: [SignalSlotMixin],
getInitialState: function(){ getInitialState: function(){
return {file: undefined}; return {file: undefined};
@ -691,13 +700,28 @@ var AudioPlayerWidget = React.createClass({
} }
else else
{ {
return( var splits = this.state.file.name.split(".");
<div> var mediatype = extensions[splits[splits.length - 1].toLowerCase()];
<p>{this.state.file.name}</p> if (mediatype == "audio") {
<audio controls src={filestreamer_url+this.state.file.hash} type="audio/mpeg"> return (
</audio> <div>
</div> <p>{this.state.file.name}</p>
); <audio controls autoPlay src={filestreamer_url+this.state.file.hash}>
</audio>
</div>
);
} else if (mediatype == "video") {
return(
<div>
<p>{this.state.file.name}</p>
<video height="300" controls autoPlay src={filestreamer_url+this.state.file.hash}>
Your browser does not support the video tag.
</video>
</div>
);
} else {
return(<div></div>);
}
} }
}, },
}); });
@ -1702,7 +1726,7 @@ var MainWidget = React.createClass({
} }
mainpage = <div> mainpage = <div>
<UnreadChatMsgsCountWidget/> <UnreadChatMsgsCountWidget/>
<AudioPlayerWidget/> <MediaPlayerWidget/>
<IdentitySelectorWidget/> <IdentitySelectorWidget/>
{mainpage} {mainpage}
</div>; </div>;

View File

@ -66,6 +66,10 @@ td{
background-color: midnightblue; background-color: midnightblue;
} }
.filelink{
color: inherit;
}
input, textarea{ input, textarea{
color: lime; color: lime;
font-family: monospace; font-family: monospace;

View File

@ -4,7 +4,15 @@ RS.start();
var api_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/api/v2/"; var api_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/api/v2/";
var filestreamer_url = window.location.protocol + "//" +window.location.hostname + ":" + window.location.port + "/fstream/"; var filestreamer_url = window.location.protocol + "//" +window.location.hostname + ":" + window.location.port + "/fstream/";
var upload_url = window.location.protocol + "//" +window.location.hostname + ":" + window.location.port + "/upload/"; var upload_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/upload/";
var extensions = {
mp3: "audio",
ogg: "audio",
wav: "audio",
webm: "video",
mp4: "video"
};
// livereload // livereload
function start_live_reload() function start_live_reload()
@ -558,7 +566,8 @@ var DownloadsWidget = React.createClass({
widget.emit("play_file", {name: file.name, hash: file.hash}) widget.emit("play_file", {name: file.name, hash: file.hash})
}; };
var playBtn = <div></div>; var playBtn = <div></div>;
if(file.name.slice(-3) === "mp3") var splits = file.name.split(".");
if(splits[splits.length-1].toLowerCase() in extensions)
playBtn = <div className="btn" onClick={playFn}>play</div>; playBtn = <div className="btn" onClick={playFn}>play</div>;
var ctrlBtn = <div></div>; var ctrlBtn = <div></div>;
@ -571,7 +580,7 @@ var DownloadsWidget = React.createClass({
ctrlBtn = <div className="btn" onClick={pauseFn}>pause</div>; ctrlBtn = <div className="btn" onClick={pauseFn}>pause</div>;
} }
return(<tr> return(<tr>
<td>{this.props.data.name}</td> <td><a className="filelink" target="_blank" href={filestreamer_url + this.props.data.hash + "/" + encodeURIComponent(this.props.data.name)}>{this.props.data.name}</a></td>
<td>{makeFriendlyUnit(this.props.data.size)}</td> <td>{makeFriendlyUnit(this.props.data.size)}</td>
<td><ProgressBar progress={this.props.data.transfered / this.props.data.size}/></td> <td><ProgressBar progress={this.props.data.transfered / this.props.data.size}/></td>
<td>{makeFriendlyUnit(this.props.data.transfer_rate*1e3)}/s</td> <td>{makeFriendlyUnit(this.props.data.transfer_rate*1e3)}/s</td>
@ -673,7 +682,7 @@ var SearchWidget = React.createClass({
}, },
}); });
var AudioPlayerWidget = React.createClass({ var MediaPlayerWidget = React.createClass({
mixins: [SignalSlotMixin], mixins: [SignalSlotMixin],
getInitialState: function(){ getInitialState: function(){
return {file: undefined}; return {file: undefined};
@ -691,13 +700,28 @@ var AudioPlayerWidget = React.createClass({
} }
else else
{ {
return( var splits = this.state.file.name.split(".");
<div> var mediatype = extensions[splits[splits.length - 1].toLowerCase()];
<p>{this.state.file.name}</p> if (mediatype == "audio") {
<audio controls src={filestreamer_url+this.state.file.hash} type="audio/mpeg"> return (
</audio> <div>
</div> <p>{this.state.file.name}</p>
); <audio controls autoPlay src={filestreamer_url+this.state.file.hash}>
</audio>
</div>
);
} else if (mediatype == "video") {
return(
<div>
<p>{this.state.file.name}</p>
<video height="300" controls autoPlay src={filestreamer_url+this.state.file.hash}>
Your browser does not support the video tag.
</video>
</div>
);
} else {
return(<div></div>);
}
} }
}, },
}); });
@ -1702,7 +1726,7 @@ var MainWidget = React.createClass({
} }
mainpage = <div> mainpage = <div>
<UnreadChatMsgsCountWidget/> <UnreadChatMsgsCountWidget/>
<AudioPlayerWidget/> <MediaPlayerWidget/>
<IdentitySelectorWidget/> <IdentitySelectorWidget/>
{mainpage} {mainpage}
</div>; </div>;