mirror of
https://github.com/RetroShare/RetroShare.git
synced 2024-10-01 02:35:48 -04:00
Merge pull request #176 from hunbernd/feature/webui-download-enhancements
Webui download and media streaming enhancements
This commit is contained in:
commit
0a21d92aca
@ -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;
|
||||||
|
@ -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
|
||||||
|
88
libresapi/src/util/ContentTypes.cpp
Normal file
88
libresapi/src/util/ContentTypes.cpp
Normal 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";
|
||||||
|
}
|
||||||
|
|
23
libresapi/src/util/ContentTypes.h
Normal file
23
libresapi/src/util/ContentTypes.h
Normal 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
|
@ -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;
|
||||||
|
@ -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>;
|
||||||
|
@ -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;
|
||||||
|
@ -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>;
|
||||||
|
Loading…
Reference in New Issue
Block a user