Updated QML and C++ in various places to build and work with new Qt (6.4+) Minor bugfixes, possibly caused by the Qt version bump, and mostly in JS logic. Tried (again) to get mod sorting working by dragging them within the list. Initial support for Profiles. (To switch between FO4 and FOLON, for example.) Initial "Launch Game" stuff, but it's far from done. Don't use it (yet).
530 lines
14 KiB
C++
530 lines
14 KiB
C++
#include "fsproxy.h"
|
|
#include "database.h"
|
|
|
|
#include <QDir>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
|
|
#define CACHE_BLOCK_SIZE (1024*1024) // 1MB chunks
|
|
|
|
FSProxy::FSProxy(QObject *parent)
|
|
: QObject{parent}
|
|
{
|
|
}
|
|
|
|
ProxyCacheEntry::ProxyCacheEntry(FSProxy *parent) :
|
|
QObject(parent),
|
|
m_parent{parent},
|
|
m_accessCount{0},
|
|
m_offset{0},
|
|
m_lastAccess{0}
|
|
{
|
|
}
|
|
|
|
void ProxyCacheEntry::clear()
|
|
{
|
|
m_mutex.lock();
|
|
for( ProxyCacheEntryChunk *c : m_chunks.values() )
|
|
c->deleteLater();
|
|
m_chunks.clear();
|
|
m_mutex.unlock();
|
|
}
|
|
|
|
void ProxyCacheEntry::clean()
|
|
{
|
|
time_t now = time(NULL) - 60; // 1 minute?
|
|
m_mutex.lock();
|
|
for( quint64 slot : m_chunks.keys() )
|
|
{
|
|
ProxyCacheEntryChunk *c = m_chunks[slot];
|
|
if( c->m_lastAccess > now )
|
|
continue;
|
|
|
|
qDebug() << "ProxyCacheEntry::clean(): Freeing chunk #" << slot;
|
|
m_chunks.take(slot)->deleteLater();
|
|
}
|
|
m_mutex.unlock();
|
|
}
|
|
|
|
FSProxyElement::FSProxyElement(const QString &path, QObject *parent)
|
|
: QObject{parent},
|
|
m_path{path}
|
|
{
|
|
}
|
|
|
|
QString FSProxyElement::resolvePath(const QString &qpath, int *matchlength=nullptr)
|
|
{
|
|
QStringList parts = qpath.split(QDir::separator());
|
|
//QString filename = parts.takeLast();
|
|
|
|
int mlen = 0;
|
|
QString bothparts = m_path;
|
|
for( QString part : parts ) {
|
|
QDir cur( bothparts );
|
|
QStringList entries = cur.entryList(QDir::AllEntries|QDir::NoDotAndDotDot);
|
|
|
|
QString thisPiece = part;
|
|
for( QString edir : entries )
|
|
{
|
|
if( 0 == part.compare(edir, Qt::CaseInsensitive) )
|
|
{
|
|
thisPiece = edir;
|
|
mlen++;
|
|
break;
|
|
}
|
|
}
|
|
bothparts.append( QDir::separator() );
|
|
bothparts.append( thisPiece );
|
|
}
|
|
|
|
//qDebug() << "FSProxy::resolvePath:" << qpath << "=>" << bothparts;
|
|
if( nullptr != matchlength )
|
|
*matchlength = mlen;
|
|
return bothparts;
|
|
}
|
|
|
|
/*
|
|
void FSProxy::setRoot(const QString &path)
|
|
{
|
|
qDebug() << "FSProxy::setRoot: Game root set to" << path;
|
|
m_path = path;
|
|
}
|
|
*/
|
|
void FSProxy::addRoot(const QString &path)
|
|
{
|
|
qDebug() << "FSProxy::setRoot: Adding proxy to" << path << "at position" << m_proxies.length();
|
|
FSProxyElement *e = new FSProxyElement(path, this);
|
|
//m_proxies.prepend( e );
|
|
m_proxies << e;
|
|
}
|
|
|
|
void FSProxy::clear()
|
|
{
|
|
qDebug() << "FSProxy: Resetting all...";
|
|
m_filesMutex.lock();
|
|
|
|
QList< FSProxyElement * > proxies = m_proxies;
|
|
m_proxies.clear();
|
|
for( FSProxyElement *e : proxies )
|
|
e->deleteLater();
|
|
|
|
QList<ProxyCacheEntry *> cache = m_cache.values();
|
|
m_cache.clear();
|
|
for( ProxyCacheEntry *e : cache )
|
|
e->deleteLater();
|
|
|
|
m_filesMutex.unlock();
|
|
}
|
|
|
|
void FSProxy::setup(Database *db, int profileId, const QString &gamename, const QString &gamedir)
|
|
{
|
|
m_db = db;
|
|
m_profileId = profileId;
|
|
m_gamename = gamename;
|
|
m_gamedir = gamedir;
|
|
reload();
|
|
}
|
|
|
|
void FSProxy::reload() {
|
|
clear();
|
|
|
|
addRoot( QString("/media/Sabrent/modding/%1/created/%2").arg(m_gamename).arg(m_profileId) );
|
|
|
|
QStringList others = m_db->proxyList(m_profileId);
|
|
for( const QString &e : others )
|
|
addRoot(e);
|
|
|
|
addRoot(m_gamedir);
|
|
}
|
|
|
|
void FSProxy::cleanCache()
|
|
{
|
|
//m_filesMutex.lock();
|
|
QList< ProxyCacheEntry * > ents = m_cache.values();
|
|
for( ProxyCacheEntry *e : ents )
|
|
e->clean();
|
|
//m_filesMutex.unlock();
|
|
}
|
|
|
|
ProxyCacheEntry *FSProxy::cacheFile(const QString &qpath)
|
|
{
|
|
int ret;
|
|
struct stat st;
|
|
for( FSProxyElement *e : m_proxies )
|
|
{
|
|
QString fullpath = e->resolvePath(qpath);
|
|
ret = ::stat(fullpath.toStdString().c_str(), &st);
|
|
if( 0 == ret ) {
|
|
qDebug() << "FSProxy::getattr: Caching" << qpath << "to" << fullpath;
|
|
ProxyCacheEntry *ne = new ProxyCacheEntry(this);
|
|
ne->m_logicalpath = qpath;
|
|
ne->m_filepath = fullpath;
|
|
memcpy( &ne->m_stat, &st, sizeof(struct stat) );
|
|
|
|
//m_filesMutex.lock();
|
|
m_cache.insert( qpath, ne );
|
|
//m_filesMutex.unlock();
|
|
|
|
return ne;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
QStringList FSProxy::readdir(const QString &qpath)
|
|
{
|
|
QStringList results;
|
|
for( FSProxyElement *e : m_proxies )
|
|
{
|
|
QString fullpath = e->resolvePath(qpath);
|
|
|
|
//qDebug() << "FSProxy::readdir(" << path << fullpath << ")";
|
|
QDir dir(fullpath);
|
|
QStringList ret = dir.entryList(QDir::AllEntries|QDir::NoDotAndDotDot);
|
|
for( const QString &qs : ret ) {
|
|
if( !results.contains(qs, Qt::CaseInsensitive) )
|
|
results << qs;
|
|
}
|
|
//qDebug() << ret;
|
|
}
|
|
return results;
|
|
}
|
|
|
|
bool FSProxy::getattr(const QString &qpath, struct stat *st)
|
|
{
|
|
if( m_cache.contains(qpath) ) {
|
|
ProxyCacheEntry *ent = m_cache[qpath];
|
|
qDebug() << "FSProxy::getattr: Using cached entry of" << qpath << "to" << ent->m_filepath;
|
|
memcpy( st, &ent->m_stat, sizeof(struct stat) );
|
|
return true;
|
|
}
|
|
|
|
ProxyCacheEntry *nent = cacheFile(qpath);
|
|
if( !nent )
|
|
return false;
|
|
|
|
memcpy( st, &nent->m_stat, sizeof(struct stat) );
|
|
return true;
|
|
}
|
|
|
|
int FSProxy::open(const QString &qpath, bool createIfNeeded)
|
|
{
|
|
if( m_cache.contains(qpath) )
|
|
{
|
|
ProxyCacheEntry *ent = m_cache[qpath];
|
|
|
|
QString fullpath = ent->m_filepath;
|
|
QFile *f = new QFile(fullpath);
|
|
//qDebug() << "FSProxy::open: (cached)" << fullpath;
|
|
if( !f->open(QIODevice::ReadWrite) )
|
|
{
|
|
qDebug() << "FSProxy::open: failed:" << f->errorString();
|
|
f->deleteLater();
|
|
return -ENODEV;
|
|
}
|
|
|
|
int fd = f->handle();
|
|
OpenFile *o = new OpenFile();
|
|
o->m_logicalpath = qpath;
|
|
o->m_filepath = fullpath;
|
|
o->m_file = f;
|
|
o->m_fd = fd;
|
|
|
|
m_filesMutex.lock();
|
|
m_openFiles.insert( fd, o );
|
|
m_filesMutex.unlock();
|
|
|
|
ent->m_accessCount++;
|
|
|
|
return fd;
|
|
}
|
|
|
|
ProxyCacheEntry *cent = cacheFile( qpath );
|
|
if( !cent && !createIfNeeded )
|
|
{
|
|
qDebug() << "FSProxy::open: 'createIfNeeded' not specified, file not found:" << qpath;
|
|
return -EACCES;
|
|
}
|
|
|
|
FSProxyElement *e = m_proxies.first();
|
|
QString fullpath = e->resolvePath(qpath);
|
|
QFile *f = new QFile(fullpath);
|
|
|
|
if( createIfNeeded ) {
|
|
QStringList parts = fullpath.split(QDir::separator());
|
|
parts.takeLast();
|
|
QDir here;
|
|
if( !here.mkpath(parts.join(QDir::separator())) )
|
|
return -EACCES;
|
|
}
|
|
|
|
qDebug() << "FSProxy::open:" << fullpath;
|
|
if( !f->open(QIODevice::ReadWrite) )
|
|
{
|
|
qDebug() << "FSProxy::open: failed:" << f->errorString();
|
|
f->deleteLater();
|
|
return -ENODEV;
|
|
}
|
|
|
|
int fd = f->handle();
|
|
OpenFile *o = new OpenFile();
|
|
o->m_logicalpath = qpath;
|
|
o->m_filepath = fullpath;
|
|
o->m_file = f;
|
|
o->m_fd = fd;
|
|
|
|
m_filesMutex.lock();
|
|
m_openFiles.insert( fd, o );
|
|
m_filesMutex.unlock();
|
|
|
|
return fd;
|
|
}
|
|
|
|
bool FSProxy::close(int fd)
|
|
{
|
|
m_filesMutex.lock();
|
|
if( !m_openFiles.contains(fd) ) {
|
|
m_filesMutex.unlock();
|
|
return false;
|
|
}
|
|
OpenFile *f = m_openFiles.take(fd);
|
|
m_filesMutex.unlock();
|
|
|
|
f->m_mutex.lock();
|
|
f->m_file->close();
|
|
f->m_file->deleteLater();
|
|
f->m_fd = -1;
|
|
f->m_file = NULL;
|
|
f->m_mutex.unlock();
|
|
f->deleteLater();
|
|
|
|
cleanCache();
|
|
|
|
return true;
|
|
}
|
|
|
|
qint64 ProxyCacheEntry::sparseRead(int fd, char *buf, size_t size, off_t offset)
|
|
{
|
|
m_accessCount++;
|
|
m_lastAccess = time(NULL);
|
|
|
|
// chunk cache is aligned on CACHE_BLOCK_SIZE, and m_chunks is indexed on multiples therein.
|
|
quint64 ipos = offset % CACHE_BLOCK_SIZE;
|
|
quint64 cstart = (offset - ipos);
|
|
quint64 cslot = cstart / CACHE_BLOCK_SIZE;
|
|
|
|
//fprintf(stderr, "ProxyCacheEntry::sparseRead: Calling, ipos=%llu / cstart=%llu / cslot=%llu / offset=%lu / size=%lu\n",
|
|
// ipos, cstart, cslot, offset, size);
|
|
|
|
quint64 rlen = CACHE_BLOCK_SIZE - ipos;
|
|
quint64 opos = 0, rtotal = 0;
|
|
while( size > 0 )
|
|
{
|
|
ProxyCacheEntryChunk *chunk;
|
|
if( m_chunks.contains(cslot) ) {
|
|
qDebug() << "ProxyCacheEntry::sparseRead: Chunk #" << cslot << "already cached. Yay!";
|
|
chunk = m_chunks[cslot];
|
|
} else {
|
|
if( !m_parent->m_openFiles.contains(fd) )
|
|
{
|
|
qDebug() << "ProxyCacheEntry::sparseRead: Attempt to read on an unknown FD!";
|
|
errno = -EBADF;
|
|
return -1;
|
|
}
|
|
|
|
OpenFile *f = m_parent->m_openFiles[fd];
|
|
if( !f->m_file->seek(cstart) )
|
|
{
|
|
qDebug() << "ProxyCacheEntry::sparseRead: Seek within file failed!";
|
|
if( 0 == opos )
|
|
{
|
|
errno = -EINVAL;
|
|
return -1;
|
|
}
|
|
return rtotal;
|
|
}
|
|
|
|
chunk = new ProxyCacheEntryChunk(cstart, this);
|
|
m_chunks[cslot] = chunk;
|
|
chunk->m_data = f->m_file->read(CACHE_BLOCK_SIZE);
|
|
qDebug() << "ProxyCacheEntry::sparseRead: Chunk #" << cslot << "cached.";
|
|
}
|
|
|
|
if( size < rlen )
|
|
rlen = size;
|
|
|
|
//fprintf(stderr, " + sparseRead(size=%lu, offset=%lu)\n", size, offset);
|
|
//fprintf(stderr, " - Copying %llu bytes of chunk+%llu to buf+%llu...\n", rlen, ipos, opos);
|
|
memcpy( buf + opos, chunk->m_data.constData() + ipos, rlen );
|
|
rtotal += rlen;
|
|
|
|
opos += rlen;
|
|
size -= rlen;
|
|
rlen = size > CACHE_BLOCK_SIZE ? CACHE_BLOCK_SIZE : size;
|
|
ipos = 0;
|
|
|
|
cstart += CACHE_BLOCK_SIZE;
|
|
cslot++;
|
|
|
|
chunk->m_accessCount++;
|
|
chunk->m_lastAccess = time(NULL);
|
|
}
|
|
|
|
//fprintf(stderr, " ~ sparseRead returning %llu bytes read.\n", rtotal);
|
|
return rtotal;
|
|
}
|
|
|
|
size_t FSProxy::read(int fd, char *buf, size_t size, off_t offset)
|
|
{
|
|
m_filesMutex.lock();
|
|
if( !m_openFiles.contains(fd) ) {
|
|
m_filesMutex.unlock();
|
|
return -EINVAL;
|
|
}
|
|
|
|
OpenFile *f = m_openFiles[fd];
|
|
f->m_mutex.lock();
|
|
m_filesMutex.unlock();
|
|
/*
|
|
ProxyCacheEntry *ent;
|
|
if( !m_cache.contains(f->m_logicalpath) )
|
|
ent = cacheFile(f->m_logicalpath);
|
|
else
|
|
ent = m_cache[f->m_logicalpath];
|
|
|
|
//size_t ret = ent->sparseRead(fd, buf, size, offset);
|
|
*/
|
|
if( !f->m_file->seek(offset) )
|
|
{
|
|
errno = -EBADF;
|
|
return -1;
|
|
}
|
|
size_t ret = f->m_file->read(buf, size);
|
|
f->m_mutex.unlock();
|
|
|
|
return ret;
|
|
}
|
|
|
|
int FSProxy::mknod(const QString &qpath, mode_t mode, dev_t dev)
|
|
{
|
|
FSProxyElement *e = m_proxies.first();
|
|
QString fullpath = e->resolvePath(qpath);
|
|
|
|
QStringList parts = fullpath.split(QDir::separator());
|
|
parts.takeLast();
|
|
QDir here;
|
|
QString parentdirs = parts.join(QDir::separator());
|
|
qDebug() << "FSProxy::mknod: Creating parent directories:" << parentdirs;
|
|
if( !here.mkpath(parentdirs) )
|
|
return -EACCES;
|
|
|
|
qDebug() << "FSProxy::mknod: mknod:" << fullpath;
|
|
return ::mknod(fullpath.toStdString().c_str(), mode, dev);
|
|
}
|
|
|
|
int FSProxy::mkdir(const QString &qpath, mode_t mode)
|
|
{
|
|
FSProxyElement *e = m_proxies.first();
|
|
QString fullpath = e->resolvePath(qpath);
|
|
return ::mkdir(fullpath.toStdString().c_str(), mode);
|
|
}
|
|
|
|
int FSProxy::unlink(const QString &qpath)
|
|
{
|
|
FSProxyElement *e = m_proxies.first();
|
|
QString fullpath = e->resolvePath(qpath);
|
|
if( m_cache.contains(qpath) )
|
|
m_cache.take(qpath)->deleteLater();
|
|
return ::unlink(fullpath.toStdString().c_str());
|
|
}
|
|
|
|
int FSProxy::rmdir(const QString &qpath)
|
|
{
|
|
FSProxyElement *e = m_proxies.first();
|
|
QString fullpath = e->resolvePath(qpath);
|
|
if( m_cache.contains(qpath) )
|
|
m_cache.take(qpath)->deleteLater();
|
|
return ::rmdir(fullpath.toStdString().c_str());
|
|
}
|
|
|
|
int FSProxy::rename(const QString &qpath, const QString &qnewname, unsigned int flags)
|
|
{
|
|
Q_UNUSED(flags)
|
|
FSProxyElement *e = m_proxies.first();
|
|
QString fullpathA = e->resolvePath(qpath);
|
|
QString fullpathB = e->resolvePath(qnewname);
|
|
if( flags & RENAME_EXCHANGE )
|
|
return -ENOTSUP;
|
|
if( flags & RENAME_NOREPLACE )
|
|
return -ENOTSUP;
|
|
if( m_cache.contains(qpath) )
|
|
m_cache.take(qpath)->deleteLater();
|
|
return ::rename(fullpathA.toStdString().c_str(), fullpathB.toStdString().c_str());
|
|
}
|
|
|
|
int FSProxy::truncate(const QString &qpath, off_t offset)
|
|
{
|
|
FSProxyElement *e = m_proxies.first();
|
|
QString fullpath = e->resolvePath(qpath);
|
|
QFileInfo fi(fullpath);
|
|
QDir dir = fi.absoluteDir();
|
|
dir.mkdir( dir.path() );
|
|
|
|
return ::truncate(fullpath.toStdString().c_str(), offset);
|
|
}
|
|
|
|
int FSProxy::write(int fd, const char *buf, size_t bufsz, off_t offset)
|
|
{
|
|
m_filesMutex.lock();
|
|
if( !m_openFiles.contains(fd) ) {
|
|
m_filesMutex.unlock();
|
|
return -EINVAL;
|
|
}
|
|
OpenFile *f = m_openFiles[fd];
|
|
f->m_mutex.lock();
|
|
m_filesMutex.unlock();
|
|
if( f->m_fd <= 0 )
|
|
{
|
|
f->m_mutex.unlock();
|
|
return -EINVAL;
|
|
}
|
|
|
|
f->m_file->seek(offset);
|
|
int ret = f->m_file->write(buf, bufsz);
|
|
if( m_cache.contains(f->m_logicalpath) )
|
|
m_cache.take(f->m_logicalpath)->deleteLater();
|
|
f->m_mutex.unlock();
|
|
return ret;
|
|
}
|
|
|
|
off_t FSProxy::lseek(int fd, off_t off, int whence)
|
|
{
|
|
m_filesMutex.lock();
|
|
if( !m_openFiles.contains(fd) ) {
|
|
m_filesMutex.unlock();
|
|
return -EINVAL;
|
|
}
|
|
OpenFile *f = m_openFiles[fd];
|
|
f->m_mutex.lock();
|
|
m_filesMutex.unlock();
|
|
if( f->m_fd <= 0 )
|
|
{
|
|
f->m_mutex.unlock();
|
|
return -EINVAL;
|
|
}
|
|
|
|
off_t ret;
|
|
if( SEEK_CUR == whence )
|
|
ret = f->m_file->seek( f->m_file->pos() + off );
|
|
else if( SEEK_END == whence )
|
|
ret = f->m_file->seek( f->m_file->size() + off );
|
|
else if( SEEK_SET == whence )
|
|
// Otherwise assume SEEK_SET
|
|
ret = f->m_file->seek(off);
|
|
else
|
|
ret = -EINVAL;
|
|
|
|
f->m_mutex.unlock();
|
|
return ret;
|
|
}
|