mirror of
https://github.com/hrydgard/ppsspp.git
synced 2024-11-23 05:19:56 +00:00
59a56d66c7
sleep_ms() should generally be avoided when possible. This can be used to try to track down unnecessary sleeps by adding some logging. This commit on its own doesn't actually add any logging.
280 lines
7.0 KiB
C++
280 lines
7.0 KiB
C++
#include <algorithm>
|
|
#include <cstring>
|
|
#include <set>
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#include "Common/Net/HTTPClient.h"
|
|
#include "Common/Net/URL.h"
|
|
|
|
#include "Common/File/PathBrowser.h"
|
|
#include "Common/File/FileUtil.h"
|
|
#include "Common/File/DirListing.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/TimeUtil.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
#include "android/jni/app-android.h"
|
|
#endif
|
|
|
|
bool LoadRemoteFileList(const Path &url, const std::string &userAgent, bool *cancel, std::vector<File::FileInfo> &files) {
|
|
_dbg_assert_(url.Type() == PathType::HTTP);
|
|
|
|
http::Client http;
|
|
Buffer result;
|
|
int code = 500;
|
|
std::vector<std::string> responseHeaders;
|
|
|
|
http.SetUserAgent(userAgent);
|
|
|
|
Url baseURL(url.ToString());
|
|
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)) {
|
|
http::RequestParams req(baseURL.Resource(), "text/plain, text/html; q=0.9, */*; q=0.8");
|
|
net::RequestProgress progress(cancel);
|
|
code = http.GET(req, &result, responseHeaders, &progress);
|
|
http.Disconnect();
|
|
}
|
|
}
|
|
|
|
if (code != 200 || (cancel && *cancel)) {
|
|
return false;
|
|
}
|
|
|
|
std::string listing;
|
|
std::vector<std::string> items;
|
|
result.TakeAll(&listing);
|
|
|
|
constexpr std::string_view ContentTypeHeader = "Content-Type:";
|
|
std::string contentType;
|
|
for (const std::string &header : responseHeaders) {
|
|
if (startsWithNoCase(header, ContentTypeHeader)) {
|
|
contentType = header.substr(ContentTypeHeader.size());
|
|
// 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 {
|
|
ERROR_LOG(Log::IO, "Unsupported Content-Type: %s", contentType.c_str());
|
|
return false;
|
|
}
|
|
Path basePath(baseURL.ToString());
|
|
for (auto &item : items) {
|
|
// Apply some workarounds.
|
|
if (item.empty())
|
|
continue;
|
|
if (item.back() == '\r') {
|
|
item.pop_back();
|
|
if (item.empty())
|
|
continue;
|
|
}
|
|
if (item == baseURL.Resource())
|
|
continue;
|
|
|
|
File::FileInfo info;
|
|
if (item.back() == '/') {
|
|
item.pop_back();
|
|
if (item.empty())
|
|
continue;
|
|
info.isDirectory = true;
|
|
} else {
|
|
info.isDirectory = false;
|
|
}
|
|
info.name = item;
|
|
info.fullName = basePath / item;
|
|
info.exists = true;
|
|
info.size = 0;
|
|
info.isWritable = false;
|
|
files.push_back(info);
|
|
}
|
|
|
|
return !files.empty();
|
|
}
|
|
|
|
PathBrowser::~PathBrowser() {
|
|
{
|
|
std::unique_lock<std::mutex> guard(pendingLock_);
|
|
pendingCancel_ = true;
|
|
pendingStop_ = true;
|
|
pendingCond_.notify_all();
|
|
}
|
|
if (pendingThread_.joinable()) {
|
|
pendingThread_.join();
|
|
}
|
|
}
|
|
|
|
void PathBrowser::SetPath(const Path &path) {
|
|
path_ = path;
|
|
ApplyRestriction();
|
|
HandlePath();
|
|
}
|
|
|
|
void PathBrowser::RestrictToRoot(const Path &root) {
|
|
INFO_LOG(Log::System, "Restricting to root: %s", root.c_str());
|
|
restrictedRoot_ = root;
|
|
}
|
|
|
|
void PathBrowser::HandlePath() {
|
|
if (!path_.empty() && path_.ToString()[0] == '!') {
|
|
if (pendingActive_)
|
|
ResetPending();
|
|
ready_ = true;
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> guard(pendingLock_);
|
|
ready_ = false;
|
|
pendingActive_ = true;
|
|
pendingCancel_ = false;
|
|
pendingFiles_.clear();
|
|
pendingPath_ = path_;
|
|
pendingCond_.notify_all();
|
|
|
|
if (pendingThread_.joinable())
|
|
return;
|
|
|
|
pendingThread_ = std::thread([&] {
|
|
SetCurrentThreadName("PathBrowser");
|
|
|
|
AndroidJNIThreadContext jniContext; // destructor detaches
|
|
|
|
std::unique_lock<std::mutex> guard(pendingLock_);
|
|
std::vector<File::FileInfo> results;
|
|
Path lastPath("NONSENSE THAT WONT EQUAL A PATH");
|
|
while (!pendingStop_) {
|
|
while (lastPath == pendingPath_ && !pendingCancel_) {
|
|
pendingCond_.wait(guard);
|
|
}
|
|
if (pendingStop_) {
|
|
break;
|
|
}
|
|
lastPath = pendingPath_;
|
|
if (lastPath.Type() == PathType::HTTP) {
|
|
guard.unlock();
|
|
results.clear();
|
|
success_ = LoadRemoteFileList(lastPath, userAgent_, &pendingCancel_, results);
|
|
guard.lock();
|
|
} else if (lastPath.empty()) {
|
|
results.clear();
|
|
success_ = true;
|
|
} else {
|
|
guard.unlock();
|
|
results.clear();
|
|
success_ = File::GetFilesInDir(lastPath, &results, nullptr);
|
|
if (!success_) {
|
|
WARN_LOG(Log::IO, "PathBrowser: Failed to list directory: %s", lastPath.c_str());
|
|
}
|
|
guard.lock();
|
|
}
|
|
|
|
if (pendingPath_ == lastPath) {
|
|
if (success_ && !pendingCancel_) {
|
|
pendingFiles_ = results;
|
|
}
|
|
pendingPath_.clear();
|
|
lastPath.clear();
|
|
ready_ = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void PathBrowser::ResetPending() {
|
|
std::lock_guard<std::mutex> guard(pendingLock_);
|
|
pendingCancel_ = true;
|
|
pendingPath_.clear();
|
|
}
|
|
|
|
std::string PathBrowser::GetFriendlyPath() const {
|
|
// Show relative to memstick root if there.
|
|
if (path_.StartsWith(aliasMatch_)) {
|
|
std::string p;
|
|
if (aliasMatch_.ComputePathTo(path_, p)) {
|
|
return aliasDisplay_ + p;
|
|
}
|
|
std::string str = path_.ToString();
|
|
if (aliasMatch_.size() < str.length()) {
|
|
return aliasDisplay_ + str.substr(aliasMatch_.size());
|
|
} else {
|
|
return aliasDisplay_;
|
|
}
|
|
}
|
|
|
|
std::string str = path_.ToString();
|
|
#if !PPSSPP_PLATFORM(ANDROID) && (PPSSPP_PLATFORM(LINUX) || PPSSPP_PLATFORM(MAC))
|
|
char *home = getenv("HOME");
|
|
if (home != nullptr && !strncmp(str.c_str(), home, strlen(home))) {
|
|
return std::string("~") + str.substr(strlen(home));
|
|
}
|
|
#endif
|
|
return path_.ToVisualString();
|
|
}
|
|
|
|
bool PathBrowser::GetListing(std::vector<File::FileInfo> &fileInfo, const char *filter, bool *cancel) {
|
|
std::unique_lock<std::mutex> guard(pendingLock_);
|
|
while (!IsListingReady() && (!cancel || !*cancel)) {
|
|
// In case cancel changes, just sleep. TODO: Replace with condition variable.
|
|
guard.unlock();
|
|
sleep_ms(50, "pathbrowser-poll");
|
|
guard.lock();
|
|
}
|
|
|
|
fileInfo = ApplyFilter(pendingFiles_, filter);
|
|
return true;
|
|
}
|
|
|
|
void PathBrowser::ApplyRestriction() {
|
|
if (!path_.StartsWith(restrictedRoot_) && !startsWith(path_.ToString(), "!")) {
|
|
WARN_LOG(Log::System, "Applying path restriction: %s (%s didn't match)", restrictedRoot_.c_str(), path_.c_str());
|
|
path_ = restrictedRoot_;
|
|
}
|
|
}
|
|
|
|
bool PathBrowser::CanNavigateUp() {
|
|
if (path_ == restrictedRoot_) {
|
|
return false;
|
|
}
|
|
return path_.CanNavigateUp();
|
|
}
|
|
|
|
void PathBrowser::NavigateUp() {
|
|
_dbg_assert_(CanNavigateUp());
|
|
path_ = path_.NavigateUp();
|
|
ApplyRestriction();
|
|
}
|
|
|
|
// TODO: Support paths like "../../hello"
|
|
void PathBrowser::Navigate(const std::string &path) {
|
|
if (path == ".")
|
|
return;
|
|
if (path == "..") {
|
|
NavigateUp();
|
|
} else {
|
|
if (path.size() >= 2 && path[1] == ':' && path_.IsRoot())
|
|
path_ = Path(path);
|
|
else
|
|
path_ = path_ / path;
|
|
}
|
|
HandlePath();
|
|
}
|