From: APTX Date: Sun, 28 Sep 2014 12:30:38 +0000 (+0200) Subject: LocalMyList FUSE filesystem. X-Git-Url: https://gitweb.tyo.aptx.org/?a=commitdiff_plain;h=7929c8f846c195a1c2b23b0e2683545c66bf7ea8;p=localmylist-fs.git LocalMyList FUSE filesystem. Initial commit. --- 7929c8f846c195a1c2b23b0e2683545c66bf7ea8 diff --git a/conversions.cpp b/conversions.cpp new file mode 100644 index 0000000..8d320ba --- /dev/null +++ b/conversions.cpp @@ -0,0 +1,16 @@ +#include "conversions.h" + +static const int SLASH = 0x2044; + +QString encodeDirectory(QString s) +{ + s.replace(QChar('/'), QChar(SLASH)); + return s; +} + +QString decodeDirectory(QString s) +{ + s.replace(QChar(SLASH), QChar('/')); + return s; + +} diff --git a/conversions.h b/conversions.h new file mode 100644 index 0000000..c8aaea6 --- /dev/null +++ b/conversions.h @@ -0,0 +1,9 @@ +#ifndef CONVERSIONS_H +#define CONVERSIONS_H + +#include + +QString encodeDirectory(QString s); +QString decodeDirectory(QString s); + +#endif // CONVERSIONS_H diff --git a/lmlfs.cpp b/lmlfs.cpp new file mode 100644 index 0000000..0ff0354 --- /dev/null +++ b/lmlfs.cpp @@ -0,0 +1,244 @@ +#include "lmlfs.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "tabledata.h" +#include "pathparser.h" +#include "querybuilder.h" +#include "conversions.h" + +#include + +static const QMap ofdMapping = []() { + QMap m; + m[PathParser::FirstUnwatchedPart] = &LocalMyList::Database::firstUnwatchedByAid; + m[PathParser::BestFilePart] = &LocalMyList::Database::openFileByEid; + m[PathParser::BestLocationPart] = &LocalMyList::Database::openFile; + return m; +}(); + +int lmlfs_getattr(const char *path, struct stat *stbuf) +{ + memset(stbuf, 0, sizeof(struct stat)); + + PathParser p; + + if (!p.parse(path)) + return -ENOENT; + + switch (p.lastPart()) { + case PathParser::RootPart: + case PathParser::TablePart: + case PathParser::ColumnPart: + case PathParser::ValuePart: + case PathParser::EntriesPart: + case PathParser::AnimeEntry: + case PathParser::EpisodeEntry: + case PathParser::FileEntry: + case PathParser::MetadataPart: + stbuf->st_mode = S_IFDIR | 0555; + break; + case PathParser::MetadataEntryPart: + stbuf->st_mode = S_IFREG | 0444; + break; + case PathParser::FirstUnwatchedPart: + case PathParser::BestFilePart: + case PathParser::BestLocationPart: + stbuf->st_mode = S_IFLNK | 0444; + break; + default: + return -ENOENT; + } + return 0; +} + +int lmlfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) +{ + (void)offset; + (void)fi; + + PathParser p; + + if (!p.parse(path)) + return -ENOENT; + + switch (p.lastPart()) { + case PathParser::RootPart: + for (const QString &table : tables) + filler(buf, table.toUtf8().data(), NULL, 0); + break; + case PathParser::TablePart: + for (const QString &column : table_columns[p.lastTable()]) + filler(buf, column.toUtf8().data(), NULL, 0); + filler(buf, Token::Entries.latin1(), NULL, 0); + break; + case PathParser::ColumnPart: { + QueryBuilder qb; + qb.buildQuery(p); + + if (!qb.isValid()) return -ENOENT; + + LocalMyList::RaiiMyList lml; + if (!lml) return -ENOTCONN; + if (!lml.connected()) return -ENOTCONN; + + QSqlQuery q = LocalMyList::instance()->database()->prepareOneShot(qb.query()); + + if (!LocalMyList::instance()->database()->exec(q)) + return -ENOENT; + + while (q.next()) { + QString value = q.value(0).toString().trimmed(); + if (value.isEmpty()) + continue; + filler(buf, encodeDirectory(value).toUtf8().data(), NULL, 0); + } + q.finish(); + } break; + case PathParser::ValuePart: + for (const QString &table : join_map[p.lastTable()].keys()) + filler(buf, table.toUtf8().data(), NULL, 0); + for (const QString &table : table_columns[p.lastTable()]) + filler(buf, table.toUtf8().data(), NULL, 0); + filler(buf, Token::Entries.latin1(), NULL, 0); + break; + case PathParser::EntriesPart: { + QueryBuilder qb; + qb.buildQuery(p); + + if (!qb.isValid()) return -ENOENT; + + LocalMyList::RaiiMyList lml; + if (!lml) return -ENOTCONN; + if (!lml.connected()) return -ENOTCONN; + + QSqlQuery q = LocalMyList::instance()->database()->prepareOneShot(qb.query()); + + if (!LocalMyList::instance()->database()->exec(q)) + return -ENOENT; + + while (q.next()) { + QString value = q.value(0).toString().trimmed(); + if (value.isEmpty()) + continue; + filler(buf, encodeDirectory(value).toUtf8().data(), NULL, 0); + } + q.finish(); + } break; + case PathParser::AnimeEntry: + filler(buf, Token::FirstUnwatched.latin1(), NULL, 0); + filler(buf, Token::Metadata.latin1(), NULL, 0); + for (const QString &table : tables) + filler(buf, table.toUtf8().data(), NULL, 0); + break; + case PathParser::EpisodeEntry: + filler(buf, Token::BestFile.latin1(), NULL, 0); + filler(buf, Token::Metadata.latin1(), NULL, 0); + for (const QString &table : join_map[p.lastTable()].keys()) + filler(buf, table.toUtf8().data(), NULL, 0); + break; + case PathParser::FileEntry: + filler(buf, Token::BestLocation.latin1(), NULL, 0); + filler(buf, Token::Metadata.latin1(), NULL, 0); + for (const QString &table : join_map[p.lastTable()].keys()) + filler(buf, table.toUtf8().data(), NULL, 0); + break; + default: + return -ENOENT; + } + + filler(buf, ".", NULL, 0); + filler(buf, "..", NULL, 0); + return 0; +} + +int lmlfs_readlink(const char *path, char *buf, size_t size) +{ + PathParser p; + + if (!p.parse(path)) return -ENOENT; + + LocalMyList::RaiiMyList lml; + if (!lml) return -ENOTCONN; + + if (!lml.connected()) return -ENOTCONN; + + QueryBuilder qb; + qb.buildQuery(p); + + *buf = '\0'; + + switch (p.lastPart()) { + case PathParser::FirstUnwatchedPart: + case PathParser::BestFilePart: + case PathParser::BestLocationPart: { + if (!qb.isValid()) return -ENOENT; + QSqlQuery q = LocalMyList::instance()->database()->prepareOneShot(qb.query()); + + if (!LocalMyList::instance()->database()->exec(q)) return -ENOENT; + if (!q.next()) return -ENOENT; + + int aid = q.value(0).toInt(); + + if (!aid) return -ENOENT; + + LocalMyList::OpenFileData ofd = (LocalMyList::instance()->database()->*ofdMapping[p.lastPart()])(aid); + if (!ofd.fid) return 0; + + qstrncpy(buf, ofd.localPath.toUtf8().data(), size); + return 0; + } break; + default: + break; + } + return -ENOENT; +} + +int lmlfs_open(const char *path, struct fuse_file_info *fi) +{ + // Currently opening files is not allowed + (void)path; + if ((fi->flags & 3) != O_RDONLY) + return -EACCES; + return -EACCES; +} + +int lmlfs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) +{ + Q_UNUSED(path); + Q_UNUSED(buf); + Q_UNUSED(size); + Q_UNUSED(offset); + Q_UNUSED(fi); + return 0; +} + +void dbg(const char *path) +{ + PathParser p; + p.parse(path); + + qDebug() << "=========================================="; + qDebug() << "valid path =" << p.isValid(); + qDebug() << "lastPart =" << p.lastPart(); + qDebug() << "properties =" << p.properties(); + + if (!p.isValid()) + return; + QueryBuilder qb; + + qb.buildQuery(p); + + qDebug() << "valid query =" << qb.isValid(); + if (!qb.isValid()) + return; + qDebug() << "query =\n" << qb.query(); +} diff --git a/lmlfs.h b/lmlfs.h new file mode 100644 index 0000000..79d3a00 --- /dev/null +++ b/lmlfs.h @@ -0,0 +1,20 @@ +#ifndef LMLFS_H +#define LMLFS_H +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int lmlfs_getattr(const char *path, struct stat *stbuf); +int lmlfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi); +int lmlfs_readlink(const char *path, char *buf, size_t size); +int lmlfs_open(const char *path, struct fuse_file_info *fi); +int lmlfs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi); + +void dbg(const char *path); + +#ifdef __cplusplus +} +#endif +#endif // LMLFS_H diff --git a/localmylist-fs.pro b/localmylist-fs.pro new file mode 100644 index 0000000..b0dbdd5 --- /dev/null +++ b/localmylist-fs.pro @@ -0,0 +1,25 @@ +TEMPLATE = app +CONFIG += console +CONFIG -= app_bundle +CONFIG += debug +QT -= gui widgets +QT *= sql +QMAKE_CXXFLAGS += -std=c++11 + +SOURCES += main.c \ + lmlfs.cpp \ + querybuilder.cpp \ + tabledata.cpp \ + pathparser.cpp \ + conversions.cpp +LIBS += -lfuse -llocalmylist +TARGET = lmlfs + +DEFINES += _FILE_OFFSET_BITS=64 + +HEADERS += \ + lmlfs.h \ + querybuilder.h \ + tabledata.h \ + pathparser.h \ + conversions.h diff --git a/main.c b/main.c new file mode 100644 index 0000000..4e039a3 --- /dev/null +++ b/main.c @@ -0,0 +1,25 @@ +#define FUSE_USE_VERSION 30 +#include +#include +#include "lmlfs.h" + +static struct fuse_operations lmlfs_oper = { + .getattr = lmlfs_getattr, + .readdir = lmlfs_readdir, + .readlink = lmlfs_readlink, + .open = lmlfs_open, + .read = lmlfs_read, +}; + +int main(int argc, char *argv[]) +{ + int i; + for (i = 0; i < argc; ++i) { + if (strcmp(argv[i], "--dbg") == 0) { + dbg(argv[argc - 1]); + return 1; + } + } + + return fuse_main(argc, argv, &lmlfs_oper, NULL); +} diff --git a/pathparser.cpp b/pathparser.cpp new file mode 100644 index 0000000..d0c2fba --- /dev/null +++ b/pathparser.cpp @@ -0,0 +1,180 @@ +#include "pathparser.h" +#include +#include "tabledata.h" +#include "conversions.h" + +#include + +PathParser::PathParser() : m_valid{false} +{ +} + +bool PathParser::parse(const char *rawPath) +{ + m_path = QString::fromUtf8(rawPath); + QStringList parts = m_path.split(QChar('/'), QString::SkipEmptyParts); + + m_lastPart = RootPart; + qDebug() << "parse " << parts; + + for (int i = 0; i < parts.length(); ++i) { + const QString &part = parts[i]; + + qDebug() << "----------------------- Iteration" << i << "-----------------------"; + qDebug() << "lastPart =" << m_lastPart; + qDebug() << "part(" << part.length() << ") =" << part; + + switch (m_lastPart) { + case RootPart: + // only "table" + if (!tables.contains(part)) { + m_valid = false; + return m_valid; + } + m_properties.insert(part, PropertyMap{}); + m_lastTable = part; + m_lastPart = TablePart; + break; + case TablePart: + // either "entries" or "column" + if (part == Token::Entries) { + m_lastPart = EntriesPart; + } else if (table_columns[m_lastTable].contains(part)) { + m_lastPart = ColumnPart; + m_lastColumn = part; + } else { + m_valid = false; + return m_valid; + } + break; + case ColumnPart: + // only "value" + m_properties[m_lastTable].insert(m_lastColumn, decodeDirectory(part)); + m_lastPart = ValuePart; + break; + case ValuePart: + // either "entries", "table" or "column" + if (part == Token::Entries) { + m_lastPart = EntriesPart; + } else if (join_map[m_lastTable].keys().contains(part)) { + m_lastPart = TablePart; + m_lastTable = part; + } else if (table_columns[m_lastTable].contains(part)) { + m_lastPart = ColumnPart; + m_lastColumn = part; + } else { + m_valid = false; + return m_valid; + } + break; + case EntriesPart: + // Only an "entry" can be in an entry list, the type depends on the last table + if (m_lastTable == Token::AnimeTable) + m_lastPart = AnimeEntry; + else if (m_lastTable == Token::EpisodeTable) + m_lastPart = EpisodeEntry; + else if (m_lastTable == Token::FileTable) + m_lastPart = FileEntry; + + // Since all entries in "entries" are unique all other conditions + // from the last table can be replaced with this one + // TODO this is not true with most of the main columns + // m_properties[m_lastTable].clear(); + m_properties[m_lastTable].insert(table_main_column[m_lastTable], decodeDirectory(part)); + break; + case AnimeEntry: + // same as "root" part, but may also contain "metadata" or "first_unwatched" + if (part == Token::Metadata) { + m_lastPart = MetadataPart; + } else if (part == Token::FirstUnwatched) { + m_lastPart = FirstUnwatchedPart; + } else if (join_map[m_lastTable].keys().contains(part)) { + m_lastPart = TablePart; + m_lastTable = part; + } else { + m_valid = false; + return m_valid; + } + break; + case EpisodeEntry: + // same as "root" part, but may also contain "metadata" or "best_file" + if (part == Token::Metadata) { + m_lastPart = MetadataPart; + } else if (part == Token::BestFile) { + m_lastPart = BestFilePart; + } else if (join_map[m_lastTable].keys().contains(part)) { + m_lastPart = TablePart; + m_lastTable = part; + } else { + m_valid = false; + return m_valid; + } + break; + case FileEntry: + // same as "root" part, but may also contain "metadata" or "best_location" + if (part == Token::Metadata) { + m_lastPart = MetadataPart; + } else if (part == Token::BestLocation) { + m_lastPart = BestLocationPart; + } else if (join_map[m_lastTable].keys().contains(part)) { + m_lastPart = TablePart; + m_lastTable = part; + } else { + m_valid = false; + return m_valid; + } + break; + case MetadataPart: + // Only contains metadata entries (files with values of all columns of the last table) + if (table_columns[m_lastTable].contains(part)) { + m_lastPart = MetadataEntryPart; + m_lastColumn = part; + } else { + m_valid = false; + return m_valid; + } + break; + // These are terminators, there can not be anything after those in a path + case MetadataEntryPart: + case FirstUnwatchedPart: + case BestFilePart: + case BestLocationPart: + default: + m_valid = false; + return m_valid; + break; + } + } + m_valid = true; + return m_valid; +} + +bool PathParser::isValid() const +{ + return m_valid; +} + +PathParser::PartType PathParser::lastPart() const +{ + return m_lastPart; +} + +QString PathParser::lastTable() const +{ + return m_lastTable; +} + +QString PathParser::lastColumn() const +{ + return m_lastColumn; +} + +QString PathParser::path() const +{ + return m_path; +} + +TablePropertyMap PathParser::properties() const +{ + return m_properties; +} diff --git a/pathparser.h b/pathparser.h new file mode 100644 index 0000000..8d3f885 --- /dev/null +++ b/pathparser.h @@ -0,0 +1,50 @@ +#ifndef PATHPARSER_H +#define PATHPARSER_H + +#include +#include + +typedef QMap PropertyMap; +typedef QMap> TablePropertyMap; + +class PathParser +{ +public: + enum PartType { + RootPart, + TablePart, + ColumnPart, + ValuePart, + EntriesPart, + AnimeEntry, + EpisodeEntry, + FileEntry, + MetadataPart, + MetadataEntryPart, + FirstUnwatchedPart, + BestFilePart, + BestLocationPart + }; + + PathParser(); + + bool parse(const char *rawPath); + + bool isValid() const; + PartType lastPart() const; + QString lastTable() const; + QString lastColumn() const; + + QString path() const; + TablePropertyMap properties() const; + +private: + QString m_path; + TablePropertyMap m_properties; + PartType m_lastPart; + QString m_lastTable; + QString m_lastColumn; + bool m_valid; +}; + +#endif // PATHPARSER_H diff --git a/querybuilder.cpp b/querybuilder.cpp new file mode 100644 index 0000000..b77f7c2 --- /dev/null +++ b/querybuilder.cpp @@ -0,0 +1,106 @@ +#include "querybuilder.h" + +#include +#include +#include "tabledata.h" + +QueryBuilder::QueryBuilder() : m_valid{false} +{ +} + +bool QueryBuilder::buildQuery(const PathParser &path) +{ + m_valid = true; + if (!path.isValid()) { + m_valid = false; + return m_valid; + } + + const TablePropertyMap &properties = path.properties(); + QString sql; + + switch (path.lastPart()) { + // Get all possible values for a column + case PathParser::ColumnPart: + sql = QString("SELECT DISTINCT %1.%2\n\tFROM %1\n").arg(path.lastTable()).arg(path.lastColumn()); + break; + case PathParser::EntriesPart: + sql = QString("SELECT DISTINCT %1.%2\n\tFROM %1\n").arg(path.lastTable()).arg(table_main_column[path.lastTable()]); + break; + case PathParser::FirstUnwatchedPart: + sql = QString("SELECT DISTINCT %1.aid\n\tFROM %1\n").arg(path.lastTable()); + break; + case PathParser::BestFilePart: + sql = QString("SELECT DISTINCT %1.eid\n\tFROM %1\n").arg(path.lastTable()); + break; + case PathParser::BestLocationPart: + // This is currently dumb, but the idea is to move away from fid as the main display table + sql = QString("SELECT DISTINCT %1.fid\n\tFROM %1\n").arg(path.lastTable()); + break; + default: + m_valid = false; + return m_valid; + break; + } + + for (const QString &table : properties.keys()) { + if (table == path.lastTable()) + continue; + + sql += buildJoin(table, path.lastTable(), properties[table]); + } + if (properties.keys().contains(path.lastTable())) { + sql += buildWhere(path.lastTable(), properties[path.lastTable()]); + } + + m_query = sql; + m_valid = true; + return m_valid; +} + +bool QueryBuilder::isValid() const +{ + return m_valid; +} + +QString QueryBuilder::query() const +{ + return m_query; +} + +QString QueryBuilder::buildJoin(const QString &table, const QString &lastTable, const PropertyMap &properties) const +{ + QString sql = QString{"\tJOIN %1 ON (%2 AND true\n%3\t)\n"}.arg(table).arg(join_map[lastTable][table]).arg(buildCondition(table, properties)); + return sql; +} + +QString QueryBuilder::buildWhere(const QString &table, const PropertyMap &properties) const +{ + if (properties.isEmpty()) + return QString(); + + QString sql = "\tWHERE true\n" + buildCondition(table, properties); + return sql; +} + +QString QueryBuilder::buildCondition(const QString &table, const PropertyMap &properties) const +{ + if (properties.isEmpty()) + return QString(); + + QString sql; + for (auto it = properties.constBegin(); it != properties.constEnd(); ++it) { + sql += QString("\t\tAND %1.%2 = %3\n").arg(table).arg(it.key()).arg(escape(it.value())); + } + return sql; +} + +QString QueryBuilder::escape(QString value) const +{ + QRegExp rx{"^[0-9]+(\\.[0-9]*)$"}; + if (value.contains(rx)) { + return value; + } + // TODO real escaping + return QChar{'\''} + value.replace(QChar{'\''}, "") + QChar{'\''}; +} diff --git a/querybuilder.h b/querybuilder.h new file mode 100644 index 0000000..1c24750 --- /dev/null +++ b/querybuilder.h @@ -0,0 +1,27 @@ +#ifndef QUERYBUILDER_H +#define QUERYBUILDER_H + +#include "pathparser.h" + +class QueryBuilder +{ +public: + QueryBuilder(); + + bool buildQuery(const PathParser &path); + + bool isValid() const; + QString query() const; + +private: + QString buildJoin(const QString &table, const QString &lastTable, const PropertyMap &properties) const; + QString buildWhere(const QString &table, const PropertyMap &properties) const; + QString buildCondition(const QString &table, const PropertyMap &properties) const; + + QString escape(QString value) const; + + QString m_query; + bool m_valid; +}; + +#endif // QUERYBUILDER_H diff --git a/tabledata.cpp b/tabledata.cpp new file mode 100644 index 0000000..9167246 --- /dev/null +++ b/tabledata.cpp @@ -0,0 +1,123 @@ +#include "tabledata.h" + +namespace Token { +const QLatin1String AnimeTable{"anime"}; +const QLatin1String EpisodeTable{"episode"}; +const QLatin1String FileTable{"file"}; +const QLatin1String Entries{"entries"}; +const QLatin1String Metadata{"metadata"}; +const QLatin1String FirstUnwatched{"fisrt_unwatched"}; +const QLatin1String BestFile{"best_file"}; +const QLatin1String BestLocation{"best_location"}; +} + +// TODO all of these shouldbe generated. +const QStringList tables = QStringList() +// TODO anime_title is currently not supported +// << "anime_title" + << "anime" + << "episode" + << "file"; + +const QMap table_main_column = []() { + QMap r; + r["anime"] = "title_romaji"; + r["episode"] = "title_english"; + r["file"] = "fid"; + return r; +}(); + +const QMap table_columns = []() { + QMap r; + r["anime"] = QStringList() + << "aid" + << "entry_added" + << "anidb_update" + << "entry_update" + << "my_update" + << "title_english" + << "title_romaji" + << "title_kanji" + << "description" + << "year" + << "start_date" + << "end_date" + << "type" + << "total_episode_count" + << "highest_epno" + << "rating" + << "votes" + << "temp_rating" + << "temp_votes" + << "my_vote" + << "my_vote_date" + << "my_temp_vote" + << "my_temp_vote_date"; + r["episode"] = QStringList() + << "eid" + << "aid" + << "entry_added" + << "anidb_update" + << "entry_update" + << "my_update" + << "epno" + << "title_english" + << "title_romaji" + << "title_kanji" + << "length" + << "airdate" + << "state" + << "type" + << "recap" + << "rating" + << "votes" + << "my_vote" + << "my_vote_date"; + r["file"] = QStringList() + << "fid" + << "eid" + << "aid" + << "gid" + << "lid" + << "entry_added" + << "anidb_update" + << "entry_update" + << "my_update" + << "ed2k" + << "size" + << "length" + << "extension" + << "group_name" + << "group_name_short" + << "crc" + << "release_date" + << "version" + << "censored" + << "deprecated" + << "source" + << "quality" + << "resolution" + << "video_codec" + << "audio_codec" + << "audio_language" + << "subtitle_language" + << "aspect_ratio" + << "my_watched" + << "my_state" + << "my_file_state" + << "my_storage" + << "my_source" + << "my_other"; + return r; +}(); + +const QMap> join_map = []() { + QMap> r; + r["anime"]["episode"] = "anime.aid = episode.aid"; + r["anime"]["file"] = "anime.aid = file.aid"; + r["episode"]["file"] = "anime.eid = episode.eid"; + r["episode"]["anime"] = "episode.aid = anime.aid"; + r["file"]["anime"] = "file.aid = anime.aid"; + r["file"]["episode"] = "file.eid = episode.eid"; + return r; +}(); diff --git a/tabledata.h b/tabledata.h new file mode 100644 index 0000000..d055d7b --- /dev/null +++ b/tabledata.h @@ -0,0 +1,24 @@ +#ifndef TABLEDATA_H +#define TABLEDATA_H + +#include +#include +#include + +namespace Token { +extern const QLatin1String AnimeTable; +extern const QLatin1String EpisodeTable; +extern const QLatin1String FileTable; +extern const QLatin1String Entries; +extern const QLatin1String Metadata; +extern const QLatin1String FirstUnwatched; +extern const QLatin1String BestFile; +extern const QLatin1String BestLocation; +} + +extern const QStringList tables; +extern const QMap table_columns; +extern const QMap table_main_column; +extern const QMap> join_map; + +#endif // TABLEDATA_H