mirror of
https://github.com/hrydgard/ppsspp.git
synced 2024-11-26 23:10:38 +00:00
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);
|
|
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();
|
|
}
|