Merge pull request #12394 from unknownbrackets/remote-iso

Allow pinning URLs to game browser
This commit is contained in:
Henrik Rydgård 2019-10-07 20:04:40 +02:00 committed by GitHub
commit 8e44230d70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 320 additions and 131 deletions

View file

@ -136,6 +136,9 @@ static bool ResolvePathVista(const std::wstring &path, wchar_t *buf, DWORD bufSi
#endif
std::string ResolvePath(const std::string &path) {
if (startsWith(path, "http://") || startsWith(path, "https://")) {
return path;
}
#ifdef _WIN32
static const int BUF_SIZE = 32768;
wchar_t *buf = new wchar_t[BUF_SIZE] {};

View file

@ -1141,8 +1141,9 @@ void Config::Load(const char *iniFileName, const char *controllerIniFilename) {
vPinnedPaths.clear();
for (auto it = pinnedPaths.begin(), end = pinnedPaths.end(); it != end; ++it) {
// Unpin paths that are deleted automatically.
if (File::Exists(it->second)) {
vPinnedPaths.push_back(File::ResolvePath(it->second));
const std::string &path = it->second;
if (startsWith(path, "http://") || startsWith(path, "https://") || File::Exists(path)) {
vPinnedPaths.push_back(File::ResolvePath(path));
}
}

View file

@ -365,11 +365,6 @@ void GameButton::Draw(UIContext &dc) {
dc.RebindTexture();
}
enum GameBrowserFlags {
FLAG_HOMEBREWSTOREBUTTON = 1
};
class DirButton : public UI::Button {
public:
DirButton(const std::string &path, UI::LayoutParams *layoutParams)
@ -440,8 +435,8 @@ void DirButton::Draw(UIContext &dc) {
}
}
GameBrowser::GameBrowser(std::string path, bool allowBrowsing, bool *gridStyle, std::string lastText, std::string lastLink, int flags, UI::LayoutParams *layoutParams)
: LinearLayout(UI::ORIENT_VERTICAL, layoutParams), gameList_(0), path_(path), gridStyle_(gridStyle), allowBrowsing_(allowBrowsing), lastText_(lastText), lastLink_(lastLink), flags_(flags) {
GameBrowser::GameBrowser(std::string path, BrowseFlags browseFlags, bool *gridStyle, std::string lastText, std::string lastLink, UI::LayoutParams *layoutParams)
: LinearLayout(UI::ORIENT_VERTICAL, layoutParams), path_(path), gridStyle_(gridStyle), browseFlags_(browseFlags), lastText_(lastText), lastLink_(lastLink) {
using namespace UI;
Refresh();
}
@ -510,10 +505,17 @@ bool GameBrowser::HasSpecialFiles(std::vector<std::string> &filenames) {
return false;
}
void GameBrowser::Update() {
LinearLayout::Update();
if (listingPending_ && path_.IsListingReady()) {
Refresh();
}
}
void GameBrowser::Refresh() {
using namespace UI;
homebrewStoreButton_ = 0;
homebrewStoreButton_ = nullptr;
// Kill all the contents
Clear();
@ -523,14 +525,14 @@ void GameBrowser::Refresh() {
// No topbar on recent screen
if (DisplayTopBar()) {
LinearLayout *topBar = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
if (allowBrowsing_) {
if (browseFlags_ & BrowseFlags::NAVIGATE) {
topBar->Add(new Spacer(2.0f));
topBar->Add(new TextView(path_.GetFriendlyPath().c_str(), ALIGN_VCENTER | FLAG_WRAP_TEXT, true, new LinearLayoutParams(FILL_PARENT, 64.0f, 1.0f)));
#if defined(USING_WIN_UI) || defined(USING_QT_UI)
topBar->Add(new Choice(mm->T("Browse", "Browse..."), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::HomeClick);
#else
topBar->Add(new Choice(mm->T("Home"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::HomeClick);
#endif
if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {
topBar->Add(new Choice(mm->T("Browse", "Browse..."), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::HomeClick);
} else {
topBar->Add(new Choice(mm->T("Home"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::HomeClick);
}
} else {
topBar->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 64.0f, 1.0f)));
}
@ -556,12 +558,14 @@ void GameBrowser::Refresh() {
std::vector<DirButton *> dirButtons;
std::vector<GameButton *> gameButtons;
listingPending_ = !path_.IsListingReady();
std::vector<std::string> filenames;
if (HasSpecialFiles(filenames)) {
for (size_t i = 0; i < filenames.size(); i++) {
gameButtons.push_back(new GameButton(filenames[i], *gridStyle_, new UI::LinearLayoutParams(*gridStyle_ == true ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT)));
}
} else {
} else if (!listingPending_) {
std::vector<FileInfo> fileInfo;
path_.GetListing(fileInfo, "iso:cso:pbp:elf:prx:ppdmp:");
for (size_t i = 0; i < fileInfo.size(); i++) {
@ -576,8 +580,8 @@ void GameBrowser::Refresh() {
isSaveData = true;
if (!isGame && !isSaveData) {
if (allowBrowsing_) {
dirButtons.push_back(new DirButton(fileInfo[i].name, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)));
if (browseFlags_ & BrowseFlags::NAVIGATE) {
dirButtons.push_back(new DirButton(fileInfo[i].fullName, fileInfo[i].name, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)));
}
} else {
gameButtons.push_back(new GameButton(fileInfo[i].fullName, *gridStyle_, new UI::LinearLayoutParams(*gridStyle_ == true ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT)));
@ -586,7 +590,7 @@ void GameBrowser::Refresh() {
// Put RAR/ZIP files at the end to get them out of the way. They're only shown so that people
// can click them and get an explanation that they need to unpack them. This is necessary due
// to a flood of support email...
if (allowBrowsing_) {
if (browseFlags_ & BrowseFlags::ARCHIVES) {
fileInfo.clear();
path_.GetListing(fileInfo, "zip:rar:r01:7z:");
if (!fileInfo.empty()) {
@ -604,7 +608,7 @@ void GameBrowser::Refresh() {
}
}
if (allowBrowsing_) {
if (browseFlags_ & BrowseFlags::NAVIGATE) {
gameList_->Add(new DirButton("..", new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)))->
OnClick.Handle(this, &GameBrowser::NavigateClick);
@ -616,6 +620,10 @@ void GameBrowser::Refresh() {
}
}
if (listingPending_) {
gameList_->Add(new UI::TextView(mm->T("Loading..."), ALIGN_CENTER, false, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)));
}
for (size_t i = 0; i < dirButtons.size(); i++) {
gameList_->Add(dirButtons[i])->OnClick.Handle(this, &GameBrowser::NavigateClick);
}
@ -632,7 +640,7 @@ void GameBrowser::Refresh() {
}
// Show a button to toggle pinning at the very end.
if (allowBrowsing_) {
if (browseFlags_ & BrowseFlags::PIN) {
std::string caption = IsCurrentPathPinned() ? "-" : "+";
if (!*gridStyle_) {
caption = IsCurrentPathPinned() ? mm->T("UnpinPath", "Unpin") : mm->T("PinPath", "Pin");
@ -641,11 +649,11 @@ void GameBrowser::Refresh() {
OnClick.Handle(this, &GameBrowser::PinToggleClick);
}
if (flags_ & FLAG_HOMEBREWSTOREBUTTON) {
if (browseFlags_ & BrowseFlags::HOMEBREW_STORE) {
Add(new Spacer());
homebrewStoreButton_ = Add(new Choice(mm->T("DownloadFromStore", "Download from the PPSSPP Homebrew Store"), new UI::LinearLayoutParams(UI::WRAP_CONTENT, UI::WRAP_CONTENT)));
} else {
homebrewStoreButton_ = 0;
homebrewStoreButton_ = nullptr;
}
if (!lastText_.empty() && gameButtons.empty()) {
@ -788,7 +796,7 @@ void MainScreen::CreateViews() {
ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
scrollRecentGames->SetTag("MainScreenRecentGames");
GameBrowser *tabRecentGames = new GameBrowser(
"!RECENT", false, &g_Config.bGridView1, "", "", 0,
"!RECENT", BrowseFlags::NONE, &g_Config.bGridView1, "", "",
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
scrollRecentGames->Add(tabRecentGames);
gameBrowsers_.push_back(tabRecentGames);
@ -806,12 +814,11 @@ void MainScreen::CreateViews() {
ScrollView *scrollHomebrew = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
scrollHomebrew->SetTag("MainScreenHomebrew");
GameBrowser *tabAllGames = new GameBrowser(g_Config.currentDirectory, true, &g_Config.bGridView2,
mm->T("How to get games"), "https://www.ppsspp.org/getgames.html", 0,
GameBrowser *tabAllGames = new GameBrowser(g_Config.currentDirectory, BrowseFlags::STANDARD, &g_Config.bGridView2,
mm->T("How to get games"), "https://www.ppsspp.org/getgames.html",
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
GameBrowser *tabHomebrew = new GameBrowser(GetSysDirectory(DIRECTORY_GAME), false, &g_Config.bGridView3,
GameBrowser *tabHomebrew = new GameBrowser(GetSysDirectory(DIRECTORY_GAME), BrowseFlags::HOMEBREW_STORE, &g_Config.bGridView3,
mm->T("How to get homebrew & demos", "How to get homebrew && demos"), "https://www.ppsspp.org/gethomebrew.html",
FLAG_HOMEBREWSTOREBUTTON,
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
Choice *hbStore = tabHomebrew->HomebrewStoreButton();
@ -1261,7 +1268,7 @@ void UmdReplaceScreen::CreateViews() {
ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
scrollRecentGames->SetTag("UmdReplaceRecentGames");
GameBrowser *tabRecentGames = new GameBrowser(
"!RECENT", false, &g_Config.bGridView1, "", "", 0,
"!RECENT", BrowseFlags::NONE, &g_Config.bGridView1, "", "",
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
scrollRecentGames->Add(tabRecentGames);
leftColumn->AddTab(mm->T("Recent"), scrollRecentGames);
@ -1271,8 +1278,8 @@ void UmdReplaceScreen::CreateViews() {
ScrollView *scrollAllGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
scrollAllGames->SetTag("UmdReplaceAllGames");
GameBrowser *tabAllGames = new GameBrowser(g_Config.currentDirectory, true, &g_Config.bGridView2,
mm->T("How to get games"), "https://www.ppsspp.org/getgames.html", 0,
GameBrowser *tabAllGames = new GameBrowser(g_Config.currentDirectory, BrowseFlags::STANDARD, &g_Config.bGridView2,
mm->T("How to get games"), "https://www.ppsspp.org/getgames.html",
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
scrollAllGames->Add(tabAllGames);

View file

@ -24,9 +24,30 @@
#include "ui/viewgroup.h"
#include "UI/MiscScreens.h"
enum GameBrowserFlags {
FLAG_HOMEBREWSTOREBUTTON = 1
};
enum class BrowseFlags {
NONE = 0,
NAVIGATE = 1,
ARCHIVES = 2,
PIN = 4,
HOMEBREW_STORE = 8,
STANDARD = 1 | 2 | 4,
};
static inline BrowseFlags operator |(const BrowseFlags &lhs, const BrowseFlags &rhs) {
return BrowseFlags((int)lhs | (int)rhs);
}
static inline bool operator &(const BrowseFlags &lhs, const BrowseFlags &rhs) {
return ((int)lhs & (int)rhs) != 0;
}
class GameBrowser : public UI::LinearLayout {
public:
GameBrowser(std::string path, bool allowBrowsing, bool *gridStyle_, std::string lastText, std::string lastLink, int flags = 0, UI::LayoutParams *layoutParams = 0);
GameBrowser(std::string path, BrowseFlags browseFlags, bool *gridStyle, std::string lastText, std::string lastLink, UI::LayoutParams *layoutParams = nullptr);
UI::Event OnChoice;
UI::Event OnHoldChoice;
@ -37,6 +58,8 @@ public:
void FocusGame(const std::string &gamePath);
void SetPath(const std::string &path);
void Update() override;
protected:
virtual bool DisplayTopBar();
virtual bool HasSpecialFiles(std::vector<std::string> &filenames);
@ -57,15 +80,15 @@ private:
UI::EventReturn HomeClick(UI::EventParams &e);
UI::EventReturn PinToggleClick(UI::EventParams &e);
UI::ViewGroup *gameList_;
UI::ViewGroup *gameList_ = nullptr;
PathBrowser path_;
bool *gridStyle_;
bool allowBrowsing_;
BrowseFlags browseFlags_;
std::string lastText_;
std::string lastLink_;
int flags_;
UI::Choice *homebrewStoreButton_;
UI::Choice *homebrewStoreButton_ = nullptr;
std::string focusGamePath_;
bool listingPending_ = false;
};
class RemoteISOBrowseScreen;

View file

@ -20,10 +20,12 @@
#include <mutex>
#include "base/timeutil.h"
#include "file/path.h"
#include "i18n/i18n.h"
#include "json/json_reader.h"
#include "net/http_client.h"
#include "net/resolve.h"
#include "net/url.h"
#include "Common/Common.h"
#include "Core/Config.h"
#include "Core/WebServer.h"
@ -39,13 +41,7 @@ static bool scanAborted = false;
static std::string RemoteSubdir() {
if (g_Config.bRemoteISOManual) {
std::string subdir = g_Config.sRemoteISOSubdir;
size_t offset = subdir.find_last_of("/");
if (offset != subdir.length() - 1 && offset != subdir.npos) {
// Truncate everything after last /
subdir.erase(offset + 1);
}
return subdir;
return g_Config.sRemoteISOSubdir;
}
return "/";
@ -142,9 +138,6 @@ static bool FindServer(std::string &resultHost, int &resultPort) {
const char *host = entry.getString("ip", "");
int port = entry.getInt("p", 0);
char url[1024] = {};
snprintf(url, sizeof(url), "http://%s:%d", host, port);
if (TryServer(host, port)) {
return true;
}
@ -154,82 +147,19 @@ static bool FindServer(std::string &resultHost, int &resultPort) {
return false;
}
static bool LoadGameList(const std::string &host, int port, std::vector<std::string> &games) {
http::Client http;
Buffer result;
int code = 500;
std::vector<std::string> responseHeaders;
// TODO: Use relative url parsing (URL::Relative) instead.
std::string subdir = RemoteSubdir();
// Start by requesting the list of games from the server.
if (http.Resolve(host.c_str(), port)) {
if (http.Connect(2, 20.0, &scanCancelled)) {
code = http.GET(subdir.c_str(), &result, responseHeaders);
http.Disconnect();
}
}
if (code != 200 || scanCancelled) {
static bool LoadGameList(const std::string &url, std::vector<std::string> &games) {
PathBrowser browser(url);
std::vector<FileInfo> files;
browser.GetListing(files, "iso:cso:pbp:elf:prx:ppdmp:", &scanCancelled);
if (scanCancelled) {
return false;
}
std::string listing;
std::vector<std::string> items;
result.TakeAll(&listing);
std::string contentType;
for (const std::string &header : responseHeaders) {
if (startsWithNoCase(header, "Content-Type:")) {
contentType = header.substr(strlen("Content-Type:"));
// Strip any whitespace (TODO: maybe move this to stringutil?)
contentType.erase(0, contentType.find_first_not_of(" \t\r\n"));
contentType.erase(contentType.find_last_not_of(" \t\r\n") + 1);
for (auto &file : files) {
if (RemoteISOFileSupported(file.name)) {
games.push_back(file.fullName);
}
}
// TODO: Technically, "TExt/hTml ; chaRSet = Utf8" should pass, but "text/htmlese" should not.
// But unlikely that'll be an issue.
bool parseHtml = startsWithNoCase(contentType, "text/html");
bool parseText = startsWithNoCase(contentType, "text/plain");
if (parseText) {
// Plain text format - easy.
SplitString(listing, '\n', items);
subdir.clear();
} else if (parseHtml) {
// Try to extract from an automatic webserver directory listing...
GetQuotedStrings(listing, items);
} else {
ERROR_LOG(FILESYS, "Unsupported Content-Type: %s", contentType.c_str());
return false;
}
for (std::string item : items) {
// Apply some workarounds.
if (item.empty())
continue;
if (item[0] != '/')
item.insert(0, "/");
if (item.back() == '\r')
item.pop_back();
if (!RemoteISOFileSupported(item)) {
continue;
}
char temp[1024] = {};
snprintf(temp, sizeof(temp) - 1, "http://%s:%d%s%s", host.c_str(), port, subdir.c_str(), item.c_str());
games.push_back(temp);
}
// Save for next time unless manual is true
if (!games.empty() && !g_Config.bRemoteISOManual) {
g_Config.sLastRemoteISOServer = host;
g_Config.iLastRemoteISOPort = port;
}
return !games.empty();
}
@ -411,7 +341,7 @@ void RemoteISOConnectScreen::update() {
case ScanStatus::LOADED:
TriggerFinish(DR_OK);
screenManager()->push(new RemoteISOBrowseScreen(games_));
screenManager()->push(new RemoteISOBrowseScreen(url_, games_));
break;
}
}
@ -432,19 +362,26 @@ ScanStatus RemoteISOConnectScreen::GetStatus() {
}
void RemoteISOConnectScreen::ExecuteLoad() {
bool result = LoadGameList(host_, port_, games_);
std::string subdir = RemoteSubdir();
url_ = StringFromFormat("http://%s:%d%s", host_.c_str(), port_, subdir.c_str());
bool result = LoadGameList(url_, games_);
if (scanAborted) {
return;
}
if (result && !games_.empty() && !g_Config.bRemoteISOManual) {
g_Config.sLastRemoteISOServer = host_;
g_Config.iLastRemoteISOPort = port_;
}
std::lock_guard<std::mutex> guard(statusLock_);
status_ = result ? ScanStatus::LOADED : ScanStatus::FAILED;
}
class RemoteGameBrowser : public GameBrowser {
public:
RemoteGameBrowser(const std::vector<std::string> &games, bool allowBrowsing, bool *gridStyle_, std::string lastText, std::string lastLink, int flags = 0, UI::LayoutParams *layoutParams = 0)
: GameBrowser("!REMOTE", allowBrowsing, gridStyle_, lastText, lastLink, flags, layoutParams) {
RemoteGameBrowser(const std::string &url, const std::vector<std::string> &games, BrowseFlags browseFlags, bool *gridStyle_, std::string lastText, std::string lastLink, UI::LayoutParams *layoutParams = nullptr)
: GameBrowser(url, browseFlags, gridStyle_, lastText, lastLink, layoutParams) {
games_ = games;
Refresh();
}
@ -456,6 +393,7 @@ protected:
bool HasSpecialFiles(std::vector<std::string> &filenames) override;
std::string url_;
std::vector<std::string> games_;
};
@ -464,7 +402,8 @@ bool RemoteGameBrowser::HasSpecialFiles(std::vector<std::string> &filenames) {
return true;
}
RemoteISOBrowseScreen::RemoteISOBrowseScreen(const std::vector<std::string> &games) : games_(games) {
RemoteISOBrowseScreen::RemoteISOBrowseScreen(const std::string &url, const std::vector<std::string> &games)
: url_(url), games_(games) {
}
void RemoteISOBrowseScreen::CreateViews() {
@ -485,7 +424,7 @@ void RemoteISOBrowseScreen::CreateViews() {
ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
scrollRecentGames->SetTag("RemoteGamesTab");
RemoteGameBrowser *tabRemoteGames = new RemoteGameBrowser(
games_, false, &g_Config.bGridView1, "", "", 0,
url_, games_, BrowseFlags::PIN, &g_Config.bGridView1, "", "",
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
scrollRecentGames->Add(tabRemoteGames);
gameBrowsers_.push_back(tabRemoteGames);
@ -592,7 +531,7 @@ UI::EventReturn RemoteISOSettingsScreen::OnChangeRemoteISOSubdir(UI::EventParams
ReplaceAll(g_Config.sRemoteISOSubdir, " ", "%20");
ReplaceAll(g_Config.sRemoteISOSubdir, "\\", "/");
//Make sure it begins with /
if (g_Config.sRemoteISOSubdir[0] != '/')
if (g_Config.sRemoteISOSubdir.empty() || g_Config.sRemoteISOSubdir[0] != '/')
g_Config.sRemoteISOSubdir = "/" + g_Config.sRemoteISOSubdir;
return UI::EVENT_DONE;

View file

@ -72,16 +72,18 @@ protected:
std::mutex statusLock_;
std::string host_;
int port_;
std::string url_;
std::vector<std::string> games_;
};
class RemoteISOBrowseScreen : public MainScreen {
public:
RemoteISOBrowseScreen(const std::vector<std::string> &games);
RemoteISOBrowseScreen(const std::string &url, const std::vector<std::string> &games);
protected:
void CreateViews() override;
std::string url_;
std::vector<std::string> games_;
};

View file

@ -43,7 +43,7 @@ std::string ResolveUrl(std::string baseUrl, std::string url) {
return baseUrl;
} else if (url[0] == '/') {
return baseUrl + url.substr(1);
} else if (url.substr(0, 7) == "http://") {
} else if (startsWith(url, "http://") || startsWith(url, "https://")) {
return url;
} else {
// Huh.

View file

@ -1,9 +1,131 @@
#include <algorithm>
#include <set>
#include "base/stringutil.h"
#include "base/timeutil.h"
#include "file/path.h"
#include "net/http_client.h"
#include "net/url.h"
#include "thread/threadutil.h"
bool LoadRemoteFileList(const std::string &url, bool *cancel, std::vector<FileInfo> &files) {
http::Client http;
Buffer result;
int code = 500;
std::vector<std::string> responseHeaders;
Url baseURL(url);
if (!baseURL.Valid()) {
return false;
}
// Start by requesting the list of files from the server.
if (http.Resolve(baseURL.Host().c_str(), baseURL.Port())) {
if (http.Connect(2, 20.0, cancel)) {
code = http.GET(baseURL.Resource().c_str(), &result, responseHeaders);
http.Disconnect();
}
}
if (code != 200 || (cancel && *cancel)) {
return false;
}
std::string listing;
std::vector<std::string> items;
result.TakeAll(&listing);
std::string contentType;
for (const std::string &header : responseHeaders) {
if (startsWithNoCase(header, "Content-Type:")) {
contentType = header.substr(strlen("Content-Type:"));
// Strip any whitespace (TODO: maybe move this to stringutil?)
contentType.erase(0, contentType.find_first_not_of(" \t\r\n"));
contentType.erase(contentType.find_last_not_of(" \t\r\n") + 1);
}
}
// TODO: Technically, "TExt/hTml ; chaRSet = Utf8" should pass, but "text/htmlese" should not.
// But unlikely that'll be an issue.
bool parseHtml = startsWithNoCase(contentType, "text/html");
bool parseText = startsWithNoCase(contentType, "text/plain");
if (parseText) {
// Plain text format - easy.
SplitString(listing, '\n', items);
} else if (parseHtml) {
// Try to extract from an automatic webserver directory listing...
GetQuotedStrings(listing, items);
} else {
ELOG("Unsupported Content-Type: %s", contentType.c_str());
return false;
}
for (std::string item : items) {
// Apply some workarounds.
if (item.empty())
continue;
if (item.back() == '\r')
item.pop_back();
if (item == baseURL.Resource())
continue;
FileInfo info;
info.name = item;
info.fullName = baseURL.Relative(item).ToString();
info.isDirectory = endsWith(item, "/");
info.exists = true;
info.size = 0;
info.isWritable = false;
files.push_back(info);
}
return !files.empty();
}
std::vector<FileInfo> ApplyFilter(std::vector<FileInfo> files, const char *filter) {
std::set<std::string> filters;
if (filter) {
std::string tmp;
while (*filter) {
if (*filter == ':') {
filters.insert(std::move(tmp));
} else {
tmp.push_back(*filter);
}
filter++;
}
if (!tmp.empty())
filters.insert(std::move(tmp));
}
auto pred = [&](const FileInfo &info) {
if (info.isDirectory || !filter)
return false;
std::string ext = getFileExtension(info.fullName);
return filters.find(ext) == filters.end();
};
files.erase(std::remove_if(files.begin(), files.end(), pred), files.end());
return files;
}
PathBrowser::~PathBrowser() {
std::unique_lock<std::mutex> guard(pendingLock_);
pendingCancel_ = true;
pendingStop_ = true;
pendingCond_.notify_all();
guard.unlock();
if (pendingThread_.joinable()) {
pendingThread_.join();
}
}
// Normalize slashes.
void PathBrowser::SetPath(const std::string &path) {
if (path[0] == '!') {
path_ = path;
HandlePath();
return;
}
path_ = path;
@ -12,9 +134,78 @@ void PathBrowser::SetPath(const std::string &path) {
}
if (!path_.size() || (path_[path_.size() - 1] != '/'))
path_ += "/";
HandlePath();
}
void PathBrowser::GetListing(std::vector<FileInfo> &fileInfo, const char *filter) {
void PathBrowser::HandlePath() {
std::lock_guard<std::mutex> guard(pendingLock_);
if (!path_.empty() && path_[0] == '!') {
ready_ = true;
pendingCancel_ = true;
pendingPath_.clear();
return;
}
if (!startsWith(path_, "http://") && !startsWith(path_, "https://")) {
ready_ = true;
pendingCancel_ = true;
pendingPath_.clear();
return;
}
ready_ = false;
pendingCancel_ = false;
pendingFiles_.clear();
pendingPath_ = path_;
pendingCond_.notify_all();
if (pendingThread_.joinable())
return;
pendingThread_ = std::thread([&] {
setCurrentThreadName("PathBrowser");
std::unique_lock<std::mutex> guard(pendingLock_);
std::vector<FileInfo> results;
std::string lastPath;
while (!pendingStop_) {
while (lastPath == pendingPath_ && !pendingCancel_) {
pendingCond_.wait(guard);
}
lastPath = pendingPath_;
bool success = false;
if (!lastPath.empty()) {
guard.unlock();
results.clear();
success = LoadRemoteFileList(lastPath, &pendingCancel_, results);
guard.lock();
}
if (pendingPath_ == lastPath) {
if (success && !pendingCancel_) {
pendingFiles_ = results;
}
pendingPath_.clear();
lastPath.clear();
ready_ = true;
}
}
});
}
bool PathBrowser::IsListingReady() {
return ready_;
}
bool PathBrowser::GetListing(std::vector<FileInfo> &fileInfo, const char *filter, bool *cancel) {
std::unique_lock<std::mutex> guard(pendingLock_);
while (!IsListingReady() && (!cancel || !*cancel)) {
// In case cancel changes, just sleep.
guard.unlock();
sleep_ms(100);
guard.lock();
}
#ifdef _WIN32
if (path_ == "/") {
// Special path that means root of file system.
@ -34,7 +225,13 @@ void PathBrowser::GetListing(std::vector<FileInfo> &fileInfo, const char *filter
}
#endif
getFilesInDir(path_.c_str(), &fileInfo, filter);
if (startsWith(path_, "http://") || startsWith(path_, "https://")) {
fileInfo = ApplyFilter(pendingFiles_, filter);
return true;
} else {
getFilesInDir(path_.c_str(), &fileInfo, filter);
return true;
}
}
// TODO: Support paths like "../../hello"
@ -52,11 +249,12 @@ void PathBrowser::Navigate(const std::string &path) {
path_ = path_.substr(0, slash + 1);
}
} else {
if (path[1] == ':' && path_ == "/")
if (path.size() > 2 && path[1] == ':' && path_ == "/")
path_ = path;
else
path_ = path_ + path;
if (path_[path_.size() - 1] != '/')
path_ += "/";
}
HandlePath();
}

View file

@ -1,7 +1,10 @@
#pragma once
#include <condition_variable>
#include <mutex>
#include <string>
#include <string.h>
#include <thread>
#include <vector>
#include <stdlib.h>
@ -14,9 +17,11 @@ class PathBrowser {
public:
PathBrowser() {}
PathBrowser(std::string path) { SetPath(path); }
~PathBrowser();
void SetPath(const std::string &path);
void GetListing(std::vector<FileInfo> &fileInfo, const char *filter = 0);
bool IsListingReady();
bool GetListing(std::vector<FileInfo> &fileInfo, const char *filter = nullptr, bool *cancel = nullptr);
void Navigate(const std::string &path);
std::string GetPath() const {
@ -39,6 +44,17 @@ public:
return str;
}
private:
void HandlePath();
std::string path_;
std::string pendingPath_;
std::vector<FileInfo> pendingFiles_;
std::condition_variable pendingCond_;
std::mutex pendingLock_;
std::thread pendingThread_;
bool pendingCancel_ = false;
bool pendingStop_ = false;
bool ready_ = false;
};