#include "seafile/fs/seafile_store.hpp" #include #include #include #include #include #include "seafile/api/account_info.hpp" #include "seafile/api/repo_info.hpp" #include "seafile/fs/storage/repo_entry.hpp" #include "seafile/fs/storage/repo_info.hpp" #include "util/application_path.hpp" #include "util/string_util.hpp" namespace seafile::fs { using RepoEntry = storage::RepoEntry; using AccountInfo = api::AccountInfo; using RepoInfo = storage::RepoInfo; namespace { std::string normalizePath(std::string path) { std::ranges::replace(path, '\\', '/'); while (path.size() > 1 && path.back() == '/') { path.pop_back(); } return path.empty() ? "/" : path; } std::vector splitPath(const std::string& path) { auto parts = shell::util::StringUtil::split(path, "/"); std::erase(parts, ""); return parts; } std::string joinPath(const std::vector& parts, const size_t start) { std::string result; for (size_t i = start; i < parts.size(); ++i) { if (!result.empty()) { result += '/'; } result += parts[i]; } return result.empty() ? "/" : "/" + result; } std::string parentPath(const std::string& path) { if (path == "/") { return "/"; } const auto pos = path.find_last_of('/'); if (pos == std::string::npos || pos == 0) { return "/"; } return path.substr(0, pos); } auto make_storage(const std::string& path) { // clang-format off return sqlite_orm::make_storage(path, sqlite_orm::make_table("account_info", sqlite_orm::make_column("space_usage", &AccountInfo::space_usage), sqlite_orm::make_column("avatar_url", &AccountInfo::avatar_url), sqlite_orm::make_column("email", &AccountInfo::email, sqlite_orm::primary_key()), sqlite_orm::make_column("name", &AccountInfo::name), sqlite_orm::make_column("total", &AccountInfo::total), sqlite_orm::make_column("usage", &AccountInfo::usage), sqlite_orm::make_column("login_id", &AccountInfo::login_id), sqlite_orm::make_column("department", &AccountInfo::department), sqlite_orm::make_column("contact_email", &AccountInfo::contact_email), sqlite_orm::make_column("institution", &AccountInfo::institution), sqlite_orm::make_column("is_staff", &AccountInfo::is_staff), sqlite_orm::make_column("file_updates_email_interval", &AccountInfo::file_updates_email_interval), sqlite_orm::make_column("collaborate_email_interval", &AccountInfo::collaborate_email_interval) ), sqlite_orm::make_table("repo_info", sqlite_orm::make_column("type", &RepoInfo::type), sqlite_orm::make_column("id", &RepoInfo::id, sqlite_orm::primary_key()), sqlite_orm::make_column("owner", &RepoInfo::owner), sqlite_orm::make_column("name", &RepoInfo::name), sqlite_orm::make_column("mtime", &RepoInfo::mtime), sqlite_orm::make_column("size", &RepoInfo::size), sqlite_orm::make_column("encrypted", &RepoInfo::encrypted), sqlite_orm::make_column("root", &RepoInfo::root), sqlite_orm::make_column("permission", &RepoInfo::permission), sqlite_orm::make_column("modifier_email", &RepoInfo::modifier_email), sqlite_orm::make_column("modifier_contact_email", &RepoInfo::modifier_contact_email), sqlite_orm::make_column("modifier_name", &RepoInfo::modifier_name), sqlite_orm::make_column("file_count", &RepoInfo::file_count), sqlite_orm::make_column("head_commit_id", &RepoInfo::head_commit_id), sqlite_orm::make_column("enc_version", &RepoInfo::enc_version), sqlite_orm::make_column("salt", &RepoInfo::salt), sqlite_orm::make_column("magic", &RepoInfo::magic), sqlite_orm::make_column("random_key", &RepoInfo::random_key) ), sqlite_orm::make_index("idx_repo_entry_path", &RepoEntry::path), sqlite_orm::make_table("repo_entry", sqlite_orm::make_column("repo_id", &RepoEntry::repo_id), sqlite_orm::make_column("type", &RepoEntry::type), sqlite_orm::make_column("name", &RepoEntry::name), sqlite_orm::make_column("path", &RepoEntry::path), sqlite_orm::make_column("id", &RepoEntry::id), sqlite_orm::make_column("mtime", &RepoEntry::mtime), sqlite_orm::make_column("modifier_email", &RepoEntry::modifier_email), sqlite_orm::make_column("size", &RepoEntry::size), sqlite_orm::make_column("is_locked", &RepoEntry::is_locked), sqlite_orm::make_column("lock_owner", &RepoEntry::lock_owner), sqlite_orm::make_column("lock_time", &RepoEntry::lock_time), sqlite_orm::make_column("locked_by_me", &RepoEntry::locked_by_me), sqlite_orm::make_column("permission", &RepoEntry::permission), sqlite_orm::make_column("modifier_contact_email", &RepoEntry::modifier_contact_email), sqlite_orm::make_column("modifier_name", &RepoEntry::modifier_name), sqlite_orm::make_column("starred", &RepoEntry::starred), sqlite_orm::primary_key(&RepoEntry::repo_id, &RepoEntry::path) ) ); //clang-format on } } struct SeafileStore::Database { using Storage = decltype(make_storage("")); Storage storage; std::recursive_mutex mutex; explicit Database(const std::string& path) : storage(make_storage(path)) { storage.pragma.journal_mode(sqlite_orm::journal_mode::WAL); storage.pragma.synchronous(1); } }; tl::expected SeafileStore::auth() { const auto auth_info = this->api.auth(this->username, this->password); if (auth_info) { return {}; } return tl::make_unexpected(api::SeafileException(auth_info.error())); } SeafileStore::SeafileStore(const std::string& db_path, const std::string& server, const std::string& user, const std::string& password) : api(server) { this->database = std::make_unique(db_path); this->username = user; this->password = password; this->api_server = server; } tl::expected SeafileStore::findRepoEntry( const std::string& repo_id, const std::string& path) { std::lock_guard lock(database->mutex); auto& storage = this->database->storage; const auto entries = storage.get_all( sqlite_orm::where( sqlite_orm::and_( sqlite_orm::c(&RepoEntry::repo_id) == repo_id, sqlite_orm::c(&RepoEntry::path) == path ) )); if (!entries.empty()) { return entries.at(0); } return tl::make_unexpected(api::SeafileException("entry not exists")); } tl::expected SeafileStore::checkIsUpdated(const std::string& repo_id) { std::lock_guard lock(database->mutex); auto& storage = this->database->storage; const auto repos = storage.get_all( sqlite_orm::where( sqlite_orm::c(&RepoInfo::id) == repo_id )); if (repos.empty()) { return false; } const auto& repo = repos.at(0); const auto& commit_id = repo.head_commit_id; const auto repo_info = this->api.repoInfo(repo_id); if (repo_info) { return repo_info.value().head_commit_id == commit_id; } return tl::make_unexpected(repo_info.error()); } tl::expected SeafileStore::checkAndSync(const std::string& repo_id) { const auto is_updated = this->checkIsUpdated(repo_id); if (!is_updated) { return tl::make_unexpected(is_updated.error()); } if (!is_updated.value()) { try { { std::lock_guard lock(database->mutex); auto& storage = this->database->storage; storage.remove_all(sqlite_orm::where( sqlite_orm::c(&RepoEntry::repo_id) == repo_id ) ); } const auto repo_entries = this->api.repoDir(repo_id, "/"); if (!repo_entries) { return tl::make_unexpected(repo_entries.error()); } { std::lock_guard lock(database->mutex); auto& storage = this->database->storage; std::vector entries; entries.reserve(repo_entries->size()); std::transform(repo_entries->begin(), repo_entries->end(), std::back_inserter(entries), [&repo_id](const auto& entry) { return RepoEntry::from_api_model(repo_id, "/", entry); }); storage.transaction([&entries,&storage] { storage.replace_range(entries.begin(), entries.end()); return true; }); } } catch (std::exception& e) { return tl::make_unexpected(api::SeafileException(e.what())); } } return {}; } tl::expected SeafileStore::init() { try { std::lock_guard lock(database->mutex); this->database->storage.sync_schema(); } catch (std::exception& e) { return tl::make_unexpected(api::SeafileException(e.what())); } if (const auto auth_token = this->auth(); !auth_token) { return tl::make_unexpected(auth_token.error()); } return this->sync(); } tl::expected SeafileStore::syncUserInfo() { const auto user_info = this->api.accountInfo(); if (!user_info) { return tl::make_unexpected(user_info.error()); } std::lock_guard lock(database->mutex); this->database->storage.replace(*user_info); return {}; } tl::expected, api::SeafileException> SeafileStore::syncRepos() { const auto repos = this->api.repos("mine", ""); if (!repos) { return tl::make_unexpected(repos.error()); } std::lock_guard lock(database->mutex); auto& storage = this->database->storage; std::vector result; result.reserve(repos->size()); for (const auto& repo : *repos) { const auto storage_repo = RepoInfo::from_api_model(repo); result.push_back(storage_repo); try { const auto existings = storage.get_all( sqlite_orm::where(sqlite_orm::c(&RepoInfo::id) == storage_repo.id)); std::optional existing = std::nullopt; if (!existings.empty()) { existing = existings.at(0); } if (existing && existing->head_commit_id != storage_repo.head_commit_id) { storage.remove_all(sqlite_orm::where( sqlite_orm::c(&RepoEntry::repo_id) == storage_repo.id)); } storage.replace(storage_repo); } catch (std::exception& e) { return tl::make_unexpected(api::SeafileException(e.what())); } } return result; } tl::expected SeafileStore::findRepoByName(const std::string& name) { try { std::vector repos; { std::lock_guard lock(database->mutex); auto& storage = this->database->storage; repos = storage.get_all( sqlite_orm::where(sqlite_orm::c(&RepoInfo::name) == name)); } if (!repos.empty()) { return repos.at(0); } // if (const auto synced = this->syncRepos(); synced) // { // std::lock_guard lock(database->mutex); // auto& storage = this->database->storage; // repos = storage.get_all( // sqlite_orm::where(sqlite_orm::c(&RepoInfo::name) == name)); // if (!repos.empty()) // { // return repos.at(0); // } // } // else // { // return tl::make_unexpected(synced.error()); // } } catch (const std::exception& e) { return tl::make_unexpected(api::SeafileException(e.what())); } return tl::make_unexpected(api::SeafileException(std::format("repo not found: {}", name))); } tl::expected SeafileStore::fetchAndStoreRepoDir( const std::string& repo_id, const std::string& parent_path) { const auto entries = this->api.repoDir(repo_id, parent_path); if (!entries) { return tl::make_unexpected(entries.error()); } try { std::vector repo_entries; repo_entries.reserve(entries->size()); std::transform(entries->begin(), entries->end(), std::back_inserter(repo_entries), [&repo_id,&parent_path](const auto& entry) { return RepoEntry::from_api_model(repo_id, parent_path, entry); }); std::lock_guard lock(database->mutex); auto& storage = this->database->storage; constexpr std::size_t batch_size = 500; storage.transaction([&] { for (auto it = repo_entries.begin(); it != repo_entries.end();) { auto end = it; std::advance(end, std::min(batch_size,static_cast(std::distance(it, repo_entries.end())))); storage.replace_range(it, end); it = end; } return true; }); } catch (std::exception& e) { return tl::make_unexpected(api::SeafileException(e.what())); } return {}; } std::vector SeafileStore::queryDirectoryChildren( const std::string& repo_id, const std::string& parent_path) { std::lock_guard lock(database->mutex); auto& storage = this->database->storage; const std::string prefix = (parent_path == "/") ? "" : parent_path; const auto all = storage.get_all( sqlite_orm::where( sqlite_orm::and_( sqlite_orm::c(&RepoEntry::repo_id) == repo_id, sqlite_orm::like(&RepoEntry::path, prefix + "/%") ) ),sqlite_orm::order_by(&RepoEntry::name)); std::vector children; for (auto& entry : all) { if (const auto rest = entry.path.substr(prefix.size() + 1); rest.find('/') == std::string::npos) { children.push_back(entry); } } return children; } tl::expected, api::SeafileException> SeafileStore::pathInfo( const std::string& path) { const auto normalized = normalizePath(path); const auto parts = splitPath(normalized); if (parts.empty()) { return tl::make_unexpected(api::SeafileException("pathInfo does not support root path")); } const auto& repo_name = parts[0]; const auto repo = findRepoByName(repo_name); if (!repo) { return tl::make_unexpected(repo.error()); } if (parts.size() == 1) { return std::variant(*repo); } const auto inner_path = joinPath(parts, 1); auto entry = findRepoEntry(repo->id, inner_path); if (entry) { return std::variant(*entry); } const auto parent_path = parentPath(inner_path); if (const auto res = fetchAndStoreRepoDir(repo->id, parent_path); !res) { return tl::make_unexpected(res.error()); } entry = findRepoEntry(repo->id, inner_path); if (entry) { return std::variant(*entry); } return tl::make_unexpected(api::SeafileException(std::format("path not found: {}", path))); } tl::expected, std::vector>, api::SeafileException> SeafileStore::listDir(const std::string& path) { const auto normalized = normalizePath(path); const auto parts = splitPath(normalized); if (parts.empty()) { std::lock_guard lock(database->mutex); auto& storage = this->database->storage; auto repos = storage.get_all(sqlite_orm::order_by(&RepoInfo::id)); if (repos.empty()) { if (const auto synced = this->syncRepos(); synced) { repos = storage.get_all(); } else { return tl::make_unexpected(synced.error()); } } return std::variant, std::vector>(std::move(repos)); } const auto& repo_name = parts[0]; const auto repo = findRepoByName(repo_name); if (!repo) { return tl::make_unexpected(repo.error()); } const auto inner_path = joinPath(parts, 1); auto children = queryDirectoryChildren(repo->id, inner_path); if (children.empty()) { if (const auto res = fetchAndStoreRepoDir(repo->id, inner_path); !res) { return tl::make_unexpected(res.error()); } children = queryDirectoryChildren(repo->id, inner_path); } return std::variant, std::vector>(std::move(children)); } tl::expected SeafileStore::readFile( const std::string& repo_id, const std::string& path, const size_t offset, const size_t length) const { if (length == 0) { return std::string{}; } return this->api.downloadFile(repo_id, path, offset, offset + length - 1); } tl::expected SeafileStore::accountInfo() const { std::lock_guard lock(database->mutex); auto& storage = this->database->storage; if (const auto infos = storage.get_all(); !infos.empty()) { return infos.front(); } return tl::make_unexpected(api::SeafileException("account info not synced")); } tl::expected SeafileStore::sync() { if (const auto sync_account = this->syncUserInfo(); !sync_account) { return tl::make_unexpected(sync_account.error()); } if (const auto sync_repos = syncRepos(); !sync_repos) { return tl::make_unexpected(sync_repos.error()); } return {}; } SeafileStore::~SeafileStore() = default; std::filesystem::path SeafileStore::storePath() { return shell::util::ApplicationPath::get().parent_path() / "seafile.sqlite"; } }