/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "storage/cache/storage_cache_database.h" #include "storage/cache/storage_cache_cleaner.h" #include "storage/storage_encryption.h" #include "storage/storage_encrypted_file.h" #include "base/flat_set.h" #include "base/flat_map.h" #include "base/algorithm.h" #include "base/concurrent_timer.h" #include #include #include #include #include namespace std { template <> struct hash { size_t operator()(const Storage::Cache::Key &key) const { return (hash()(key.high) ^ hash()(key.low)); } }; } // namespace std namespace Storage { namespace Cache { namespace details { namespace { using RecordType = uint8; using PlaceId = std::array; using EntrySize = std::array; using RecordsCount = std::array; constexpr auto kRecordSizeUnknown = size_type(-1); constexpr auto kRecordSizeInvalid = size_type(-2); constexpr auto kBundledRecordsLimit = (1 << (RecordsCount().size() * 8)); constexpr auto kDataSizeLimit = (1 << (EntrySize().size() * 8)); template constexpr auto GoodForEncryption = ((sizeof(Record) & 0x0F) == 0); template Packed ReadTo(size_type count) { Expects(count >= 0 && count < (1 << (Packed().size() * 8))); auto result = Packed(); for (auto &element : result) { element = uint8(count & 0xFF); count >>= 8; } return result; } template size_type ReadFrom(Packed count) { auto result = size_type(); RecordsCount a; for (auto &element : (count | ranges::view::reverse)) { result <<= 8; result |= size_type(element); } return result; } uint32 CountChecksum(bytes::const_span data) { const auto seed = uint32(0); return XXH32(data.data(), data.size(), seed); } QString PlaceFromId(PlaceId place) { auto result = QString(); result.reserve(15); const auto pushDigit = [&](uint8 digit) { const auto hex = (digit < 0x0A) ? char('0' + digit) : char('A' + (digit - 0x0A)); result.push_back(hex); }; const auto push = [&](uint8 value) { pushDigit(value & 0x0F); pushDigit(value >> 4); }; for (auto i = 0; i != place.size(); ++i) { push(place[i]); if (!i) { result.push_back('/'); } } return result; } int32 GetUnixtime() { return int32(time(nullptr)); } enum class Format : uint32 { Format_0, }; struct BasicHeader { BasicHeader(); Format format : 8; uint32 flags : 24; uint32 date = 0; uint32 reserved1 = 0; uint32 reserved2 = 0; }; static_assert(GoodForEncryption); BasicHeader::BasicHeader() : format(Format::Format_0) , flags(0) { } struct Store { static constexpr auto kType = RecordType(0x01); RecordType type = kType; uint8 tag = 0; EntrySize size = { { 0 } }; PlaceId place = { { 0 } }; uint32 checksum = 0; Key key; }; static_assert(GoodForEncryption); struct MultiStoreHeader { static constexpr auto kType = RecordType(0x02); explicit MultiStoreHeader(size_type count = 0); RecordType type = kType; RecordsCount count = { { 0 } }; uint32 reserved1 = 0; uint32 reserved2 = 0; uint32 reserved3 = 0; }; struct MultiStorePart { uint8 reserved = 0; uint8 tag = 0; EntrySize size = { { 0 } }; PlaceId place = { { 0 } }; uint32 checksum = 0; Key key; }; static_assert(GoodForEncryption); static_assert(GoodForEncryption); MultiStoreHeader::MultiStoreHeader(size_type count) : type(kType) , count(ReadTo(count)) { Expects(count >= 0 && count < kBundledRecordsLimit); } struct MultiRemoveHeader { static constexpr auto kType = RecordType(0x03); explicit MultiRemoveHeader(size_type count = 0); RecordType type = kType; RecordsCount count = { { 0 } }; uint32 reserved1 = 0; uint32 reserved2 = 0; uint32 reserved3 = 0; }; struct MultiRemovePart { Key key; }; static_assert(GoodForEncryption); static_assert(GoodForEncryption); MultiRemoveHeader::MultiRemoveHeader(size_type count) : type(kType) , count(ReadTo(count)) { Expects(count >= 0 && count < kBundledRecordsLimit); } struct MultiTouchedHeader { static constexpr auto kType = RecordType(0x04); MultiTouchedHeader( uint32 time, uint32 advancement, size_type count = 0); RecordType type = kType; RecordsCount count = { { 0 } }; uint32 timeAdvancement = 0; uint32 systemTime = 0; uint32 reserved = 0; }; struct MultiTouchedPart { Key key; }; static_assert(GoodForEncryption); static_assert(GoodForEncryption); MultiTouchedHeader::MultiTouchedHeader( uint32 time, uint32 advancement, size_type count) : type(kType) , count(ReadTo(count)) , timeAdvancement(advancement) , systemTime(time) { Expects(count >= 0 && count < kBundledRecordsLimit); } } // namespace class Database { public: using Wrapper = Cache::Database; using Settings = Wrapper::Settings; Database( crl::weak_on_queue weak, const QString &path, const Settings &settings); void open(EncryptionKey key, FnMut done); void close(FnMut done); void put(const Key &key, QByteArray value, FnMut done); void get(const Key &key, FnMut done); void remove(const Key &key, FnMut done = nullptr); void clear(FnMut done); ~Database(); private: struct Entry { Entry() = default; Entry( PlaceId place, uint8 tag, uint32 checksum, size_type size, int64 useTime); int64 useTime = 0; size_type size = 0; uint32 checksum = 0; PlaceId place = { { 0 } }; uint8 tag = 0; }; struct CleanerWrap { std::unique_ptr object; base::binary_guard guard; }; using Map = std::unordered_map; template void invokeCallback(Callback &&callback, Args &&...args); Error ioError(const QString &path) const; QString computePath(Version version) const; QString binlogPath(Version version) const; QString binlogPath() const; QString binlogFilename() const; File::Result openBinlog( Version version, File::Mode mode, EncryptionKey &key); bool readHeader(); bool writeHeader(); void readBinlog(); size_type readBinlogRecords(bytes::const_span data); size_type readBinlogRecordSize(bytes::const_span data) const; bool readBinlogRecord(bytes::const_span data); bool readRecordStore(bytes::const_span data); bool readRecordMultiStore(bytes::const_span data); bool readRecordMultiRemove(bytes::const_span data); bool readRecordMultiTouched(bytes::const_span data); void adjustRelativeTime(); void startDelayedPruning(); int64 countRelativeTime() const; int64 pruneBeforeTime() const; void prune(); void collectTimePrune( base::flat_set &stale, int64 &staleTotalSize); void collectSizePrune( base::flat_set &stale, int64 &staleTotalSize); void setMapEntry(const Key &key, Entry &&entry); void eraseMapEntry(const Map::const_iterator &i); Version findAvailableVersion() const; QString versionPath() const; bool writeVersion(Version version); Version readVersion() const; QString placePath(PlaceId place) const; bool isFreePlace(PlaceId place) const; QString writeKeyPlace(const Key &key, size_type size, uint32 checksum); void writeMultiRemoveLazy(); void writeMultiRemove(); void writeMultiTouchedLazy(); void writeMultiTouched(); void writeMultiTouchedBlock(); void writeBundlesLazy(); void writeBundles(); void createCleaner(); void cleanerDone(Error error); crl::weak_on_queue _weak; QString _base, _path; Settings _settings; EncryptionKey _key; File _binlog; Map _map; std::set _removing; std::set _touched; int64 _relativeTime = 0; int64 _timeCorrection = 0; uint32 _latestSystemTime = 0; int64 _totalSize = 0; int64 _minimalEntryTime = 0; size_type _entriesWithMinimalTimeCount = 0; base::ConcurrentTimer _writeBundlesTimer; base::ConcurrentTimer _pruneTimer; CleanerWrap _cleaner; }; Database::Entry::Entry( PlaceId place, uint8 tag, uint32 checksum, size_type size, int64 useTime) : useTime(useTime) , size(size) , checksum(checksum) , tag(tag) , place(place) { } Database::Database( crl::weak_on_queue weak, const QString &path, const Settings &settings) : _weak(std::move(weak)) , _base(ComputeBasePath(path)) , _settings(settings) , _writeBundlesTimer(_weak, [=] { writeBundles(); }) , _pruneTimer(_weak, [=] { prune(); }) { Expects(_settings.maxDataSize < kDataSizeLimit); Expects(_settings.maxBundledRecords < kBundledRecordsLimit); Expects(!_settings.totalTimeLimit || _settings.totalTimeLimit > 0); Expects(!_settings.totalSizeLimit || _settings.totalSizeLimit > _settings.maxDataSize); } template void Database::invokeCallback(Callback &&callback, Args &&...args) { if (callback) { callback(std::move(args)...); } } Error Database::ioError(const QString &path) const { return { Error::Type::IO, path }; } void Database::open(EncryptionKey key, FnMut done) { const auto version = readVersion(); const auto result = openBinlog(version, File::Mode::ReadAppend, key); switch (result) { case File::Result::Success: invokeCallback(done, Error::NoError()); break; case File::Result::LockFailed: invokeCallback( done, Error{ Error::Type::LockFailed, binlogPath(version) }); break; case File::Result::WrongKey: invokeCallback( done, Error{ Error::Type::WrongKey, binlogPath(version) }); break; case File::Result::Failed: { const auto available = findAvailableVersion(); if (writeVersion(available)) { const auto open = openBinlog(available, File::Mode::Write, key); if (open == File::Result::Success) { invokeCallback(done, Error::NoError()); } else { invokeCallback(done, ioError(binlogPath(available))); } } else { invokeCallback(done, ioError(versionPath())); } } break; default: Unexpected("Result from Database::openBinlog."); } } QString Database::computePath(Version version) const { return _base + QString::number(version) + '/'; } QString Database::binlogFilename() const { return QStringLiteral("binlog"); } QString Database::binlogPath(Version version) const { return computePath(version) + binlogFilename(); } QString Database::binlogPath() const { return _path + binlogFilename(); } File::Result Database::openBinlog( Version version, File::Mode mode, EncryptionKey &key) { const auto path = binlogPath(version); const auto result = _binlog.open(path, mode, key); if (result == File::Result::Success) { const auto headerRequired = (mode == File::Mode::Read) || (mode == File::Mode::ReadAppend && _binlog.size() > 0); if (headerRequired ? readHeader() : writeHeader()) { _path = computePath(version); _key = std::move(key); createCleaner(); readBinlog(); } else { return File::Result::Failed; } } return result; } bool Database::readHeader() { auto header = BasicHeader(); if (_binlog.read(bytes::object_as_span(&header)) != sizeof(header)) { return false; } else if (header.format != Format::Format_0) { return false; } _relativeTime = _latestSystemTime = header.date; return true; } bool Database::writeHeader() { auto header = BasicHeader(); const auto now = std::max(GetUnixtime(), 1); _relativeTime = _latestSystemTime = header.date = now; return _binlog.write(bytes::object_as_span(&header)); } void Database::readBinlog() { auto data = bytes::vector(_settings.readBlockSize); const auto full = bytes::make_span(data); auto notParsedBytes = index_type(0); while (true) { Assert(notParsedBytes < full.size()); const auto readBytes = _binlog.read(full.subspan(notParsedBytes)); if (!readBytes) { break; } notParsedBytes += readBytes; const auto bytes = full.subspan(0, notParsedBytes); const auto parsedTill = readBinlogRecords(bytes); if (parsedTill == kRecordSizeInvalid) { break; } Assert(parsedTill >= 0 && parsedTill <= notParsedBytes); notParsedBytes -= parsedTill; if (parsedTill > 0 && parsedTill < bytes.size()) { bytes::move(full, bytes.subspan(parsedTill)); } } _binlog.seek(_binlog.offset() - notParsedBytes); adjustRelativeTime(); startDelayedPruning(); } int64 Database::countRelativeTime() const { const auto now = std::max(GetUnixtime(), 1); const auto delta = std::max(int64(now) - int64(_latestSystemTime), 0LL); return _relativeTime + delta; } int64 Database::pruneBeforeTime() const { return _settings.totalTimeLimit ? (countRelativeTime() - _settings.totalTimeLimit) : 0LL; } void Database::startDelayedPruning() { if (_map.empty()) { return; } const auto pruning = [&] { if (_settings.totalSizeLimit > 0 && _totalSize > _settings.totalSizeLimit) { return true; } else if (_minimalEntryTime != 0 && _minimalEntryTime <= pruneBeforeTime()) { return true; } return false; }(); if (pruning) { if (!_pruneTimer.isActive() || _pruneTimer.remainingTime() > _settings.pruneTimeout) { _pruneTimer.callOnce(_settings.pruneTimeout); } } else if (_minimalEntryTime != 0) { const auto before = pruneBeforeTime(); const auto seconds = (_minimalEntryTime - before); if (!_pruneTimer.isActive()) { _pruneTimer.callOnce(std::min( seconds * crl::time_type(1000), _settings.maxPruneCheckTimeout)); } } } void Database::prune() { auto stale = base::flat_set(); auto staleTotalSize = int64(); collectTimePrune(stale, staleTotalSize); collectSizePrune(stale, staleTotalSize); for (const auto &key : stale) { remove(key); } startDelayedPruning(); } void Database::collectTimePrune( base::flat_set &stale, int64 &staleTotalSize) { if (!_settings.totalTimeLimit) { return; } const auto before = pruneBeforeTime(); if (!_minimalEntryTime || _minimalEntryTime > before) { return; } _minimalEntryTime = 0; _entriesWithMinimalTimeCount = 0; for (const auto &[key, entry] : _map) { if (entry.useTime <= before) { stale.emplace(key); staleTotalSize += entry.size; } else if (!_minimalEntryTime || _minimalEntryTime > entry.useTime) { _minimalEntryTime = entry.useTime; _entriesWithMinimalTimeCount = 1; } else if (_minimalEntryTime == entry.useTime) { ++_entriesWithMinimalTimeCount; } } } void Database::collectSizePrune( base::flat_set &stale, int64 &staleTotalSize) { const auto removeSize = (_settings.totalSizeLimit > 0) ? (_totalSize - staleTotalSize - _settings.totalSizeLimit) : 0; if (removeSize <= 0) { return; } using Bucket = std::pair; auto oldest = base::flat_multi_map< int64, const Bucket*, std::greater<>>(); auto oldestTotalSize = int64(); const auto canRemoveFirst = [&](const Entry &adding) { const auto totalSizeAfterAdd = oldestTotalSize + adding.size; const auto &first = oldest.begin()->second->second; return (adding.useTime <= first.useTime && (totalSizeAfterAdd - removeSize >= first.size)); }; for (const auto &bucket : _map) { const auto &entry = bucket.second; if (stale.contains(bucket.first)) { continue; } const auto add = (oldestTotalSize < removeSize) ? true : (entry.useTime < oldest.begin()->second->second.useTime); if (!add) { continue; } while (!oldest.empty() && canRemoveFirst(entry)) { oldestTotalSize -= oldest.begin()->second->second.size; oldest.erase(oldest.begin()); } oldestTotalSize += entry.size; oldest.emplace(entry.useTime, &bucket); } for (const auto &pair : oldest) { stale.emplace(pair.second->first); } staleTotalSize += oldestTotalSize; } void Database::adjustRelativeTime() { const auto now = std::max(GetUnixtime(), 1); if (now < _latestSystemTime) { writeMultiTouchedBlock(); } } size_type Database::readBinlogRecords(bytes::const_span data) { auto result = 0; while (true) { const auto size = readBinlogRecordSize(data); if (size == kRecordSizeUnknown || size > data.size()) { return result; } else if (size == kRecordSizeInvalid || !readBinlogRecord(data)) { return (result > 0) ? result : kRecordSizeInvalid; } else { result += size; data = data.subspan(size); } } } size_type Database::readBinlogRecordSize(bytes::const_span data) const { if (data.empty()) { return kRecordSizeUnknown; } switch (static_cast(data[0])) { case Store::kType: return sizeof(Store); case MultiStoreHeader::kType: if (data.size() >= sizeof(MultiStoreHeader)) { const auto header = reinterpret_cast( data.data()); const auto count = ReadFrom(header->count); return (count > 0 && count < _settings.maxBundledRecords) ? (sizeof(MultiStoreHeader) + count * sizeof(MultiStorePart)) : kRecordSizeInvalid; } return kRecordSizeUnknown; case MultiRemoveHeader::kType: if (data.size() >= sizeof(MultiRemoveHeader)) { const auto header = reinterpret_cast( data.data()); const auto count = ReadFrom(header->count); return (count > 0 && count < _settings.maxBundledRecords) ? (sizeof(MultiRemoveHeader) + count * sizeof(MultiRemovePart)) : kRecordSizeInvalid; } return kRecordSizeUnknown; case MultiTouchedHeader::kType: if (data.size() >= sizeof(MultiTouchedHeader)) { const auto header = reinterpret_cast( data.data()); const auto count = ReadFrom(header->count); return (count > 0 && count < _settings.maxBundledRecords) ? (sizeof(MultiTouchedHeader) + count * sizeof(MultiTouchedPart)) : kRecordSizeInvalid; } return kRecordSizeUnknown; } return kRecordSizeInvalid; } bool Database::readBinlogRecord(bytes::const_span data) { Expects(!data.empty()); switch (static_cast(data[0])) { case Store::kType: return readRecordStore(data); case MultiStoreHeader::kType: return readRecordMultiStore(data); case MultiRemoveHeader::kType: return readRecordMultiRemove(data); case MultiTouchedHeader::kType: return readRecordMultiTouched(data); } Unexpected("Bad type in Database::readBinlogRecord."); } bool Database::readRecordStore(bytes::const_span data) { Expects(data.size() >= sizeof(Store)); const auto record = reinterpret_cast(data.data()); const auto size = ReadFrom(record->size); if (size > _settings.maxDataSize) { return false; } setMapEntry( record->key, Entry( record->place, record->tag, record->checksum, size, _relativeTime)); return true; } bool Database::readRecordMultiStore(bytes::const_span data) { Expects(data.size() >= sizeof(MultiStoreHeader)); const auto bytes = data.data(); const auto record = reinterpret_cast(bytes); const auto count = ReadFrom(record->count); Assert(data.size() >= sizeof(MultiStoreHeader) + count * sizeof(MultiStorePart)); const auto parts = gsl::make_span( reinterpret_cast( bytes + sizeof(MultiStoreHeader)), count); for (const auto &part : parts) { const auto size = ReadFrom(part.size); if (size > _settings.maxDataSize) { return false; } setMapEntry( part.key, Entry( part.place, part.tag, part.checksum, size, _relativeTime)); } return true; } void Database::setMapEntry(const Key &key, Entry &&entry) { auto &already = _map[key]; _totalSize += entry.size - already.size; if (entry.useTime != 0 && (entry.useTime < _minimalEntryTime || !_minimalEntryTime)) { _minimalEntryTime = entry.useTime; _entriesWithMinimalTimeCount = 1; } else if (_minimalEntryTime != 0 && already.useTime != entry.useTime) { if (entry.useTime == _minimalEntryTime) { Assert(_entriesWithMinimalTimeCount > 0); ++_entriesWithMinimalTimeCount; } else if (already.useTime == _minimalEntryTime) { Assert(_entriesWithMinimalTimeCount > 0); --_entriesWithMinimalTimeCount; } } already = std::move(entry); } void Database::eraseMapEntry(const Map::const_iterator &i) { if (i != end(_map)) { const auto &entry = i->second; _totalSize -= entry.size; if (_minimalEntryTime != 0 && entry.useTime == _minimalEntryTime) { Assert(_entriesWithMinimalTimeCount > 0); --_entriesWithMinimalTimeCount; } _map.erase(i); } } bool Database::readRecordMultiRemove(bytes::const_span data) { Expects(data.size() >= sizeof(MultiRemoveHeader)); const auto bytes = data.data(); const auto record = reinterpret_cast(bytes); const auto count = ReadFrom(record->count); Assert(data.size() >= sizeof(MultiRemoveHeader) + count * sizeof(MultiRemovePart)); const auto parts = gsl::make_span( reinterpret_cast( bytes + sizeof(MultiRemoveHeader)), count); for (const auto &part : parts) { eraseMapEntry(_map.find(part.key)); } return true; } bool Database::readRecordMultiTouched(bytes::const_span data) { Expects(data.size() >= sizeof(MultiTouchedHeader)); const auto bytes = data.data(); const auto record = reinterpret_cast(bytes); if (record->timeAdvancement > _settings.maxTimeAdvancement) { return false; } _relativeTime += record->timeAdvancement; _latestSystemTime = record->systemTime; const auto count = ReadFrom(record->count); Assert(data.size() >= sizeof(MultiTouchedHeader) + count * sizeof(MultiTouchedPart)); const auto parts = gsl::make_span( reinterpret_cast( bytes + sizeof(MultiTouchedHeader)), count); for (const auto &part : parts) { if (const auto i = _map.find(part.key); i != end(_map)) { i->second.useTime = _relativeTime; } } return true; } void Database::close(FnMut done) { writeBundles(); _cleaner = CleanerWrap(); _binlog.close(); invokeCallback(done); } void Database::put( const Key &key, QByteArray value, FnMut done) { _removing.erase(key); const auto checksum = CountChecksum(bytes::make_span(value)); const auto path = writeKeyPlace(key, value.size(), checksum); if (path.isEmpty()) { invokeCallback(done, ioError(binlogPath())); return; } File data; const auto result = data.open(path, File::Mode::Write, _key); switch (result) { case File::Result::Failed: invokeCallback(done, ioError(path)); break; case File::Result::LockFailed: invokeCallback(done, Error{ Error::Type::LockFailed, path }); break; case File::Result::Success: { const auto success = data.writeWithPadding(bytes::make_span(value)); if (!success) { data.close(); remove(key, nullptr); invokeCallback(done, ioError(path)); } else { data.flush(); invokeCallback(done, Error::NoError()); _touched.emplace(key); writeMultiTouchedLazy(); startDelayedPruning(); } } break; default: Unexpected("Result in Database::put."); } } QString Database::writeKeyPlace( const Key &key, size_type size, uint32 checksum) { Expects(size <= _settings.maxDataSize); auto record = Store(); record.key = key; record.size = ReadTo(size); record.checksum = checksum; if (const auto i = _map.find(key); i != end(_map)) { record.place = i->second.place; } else { do { bytes::set_random(bytes::object_as_span(&record.place)); } while (!isFreePlace(record.place)); } const auto result = placePath(record.place); auto writeable = record; const auto success = _binlog.write(bytes::object_as_span(&writeable)); if (!success) { return QString(); } _binlog.flush(); readRecordStore(bytes::object_as_span(&record)); return result; } void Database::get(const Key &key, FnMut done) { if (_removing.find(key) != end(_removing)) { invokeCallback(done, QByteArray()); return; } const auto i = _map.find(key); if (i == _map.end()) { invokeCallback(done, QByteArray()); return; } const auto &entry = i->second; const auto path = placePath(entry.place); File data; const auto result = data.open(path, File::Mode::Read, _key); switch (result) { case File::Result::Failed: invokeCallback(done, QByteArray()); break; case File::Result::WrongKey: invokeCallback(done, QByteArray()); break; case File::Result::Success: { auto result = QByteArray(entry.size, Qt::Uninitialized); const auto bytes = bytes::make_span(result); const auto read = data.readWithPadding(bytes); if (read != entry.size || CountChecksum(bytes) != entry.checksum) { invokeCallback(done, QByteArray()); } else { invokeCallback(done, std::move(result)); _touched.emplace(key); writeMultiTouchedLazy(); startDelayedPruning(); } } break; default: Unexpected("Result in Database::get."); } } void Database::remove(const Key &key, FnMut done) { const auto i = _map.find(key); if (i != _map.end()) { _removing.emplace(key); writeMultiRemoveLazy(); const auto path = placePath(i->second.place); eraseMapEntry(i); QFile(path).remove(); } invokeCallback(done); } void Database::writeBundlesLazy() { if (!_writeBundlesTimer.isActive()) { _writeBundlesTimer.callOnce(_settings.writeBundleDelay); } } void Database::writeMultiRemoveLazy() { if (_removing.size() == _settings.maxBundledRecords) { writeMultiRemove(); } else { writeBundlesLazy(); } } void Database::writeMultiRemove() { Expects(_removing.size() <= _settings.maxBundledRecords); if (_removing.empty()) { return; } const auto size = _removing.size(); auto header = MultiRemoveHeader(size); auto list = std::vector(); list.reserve(size); for (const auto &key : base::take(_removing)) { list.push_back({ key }); } if (_binlog.write(bytes::object_as_span(&header))) { _binlog.write(bytes::make_span(list)); _binlog.flush(); } } void Database::writeMultiTouchedLazy() { if (_touched.size() == _settings.maxBundledRecords) { writeMultiTouched(); } else { writeBundlesLazy(); } } void Database::writeMultiTouched() { if (!_touched.empty()) { writeMultiTouchedBlock(); } } void Database::writeMultiTouchedBlock() { Expects(_touched.size() <= _settings.maxBundledRecords); const auto now = std::max(GetUnixtime(), 1); const auto delta = std::max(int64(now) - int64(_latestSystemTime), 0LL); const auto advancement = std::min( delta, int64(_settings.maxTimeAdvancement)); const auto size = _touched.size(); auto header = MultiTouchedHeader(now, advancement, size); auto list = std::vector(); if (size > 0) { list.reserve(size); for (const auto &key : base::take(_touched)) { list.push_back({ key }); } } _latestSystemTime = now; if (advancement > 0) { _relativeTime += advancement; for (const auto &entry : list) { if (const auto i = _map.find(entry.key); i != end(_map)) { i->second.useTime = _relativeTime; } } } if (_binlog.write(bytes::object_as_span(&header))) { if (size > 0) { _binlog.write(bytes::make_span(list)); } _binlog.flush(); } } void Database::writeBundles() { writeMultiRemove(); writeMultiTouched(); } void Database::createCleaner() { auto [left, right] = base::make_binary_guard(); _cleaner.guard = std::move(left); auto done = [weak = _weak](Error error) { weak.with([=](Database &that) { that.cleanerDone(error); }); }; _cleaner.object = std::make_unique( _base, std::move(right), std::move(done)); } void Database::cleanerDone(Error error) { _cleaner = CleanerWrap(); } void Database::clear(FnMut done) { Expects(_key.empty()); const auto version = findAvailableVersion(); invokeCallback( done, writeVersion(version) ? Error::NoError() : ioError(versionPath())); } Database::~Database() { close(nullptr); } auto Database::findAvailableVersion() const -> Version { const auto entries = QDir(_base).entryList( QDir::Dirs | QDir::NoDotAndDotDot); auto versions = base::flat_set(); for (const auto entry : entries) { versions.insert(entry.toInt()); } auto result = Version(); for (const auto version : versions) { if (result != version) { break; } ++result; } return result; } QString Database::versionPath() const { return VersionFilePath(_base); } bool Database::writeVersion(Version version) { return WriteVersionValue(_base, version); } auto Database::readVersion() const -> Version { if (const auto result = ReadVersionValue(_base)) { return *result; } return Version(); } QString Database::placePath(PlaceId place) const { return _path + PlaceFromId(place); } bool Database::isFreePlace(PlaceId place) const { return !QFile(placePath(place)).exists(); } } // namespace details Database::Database(const QString &path, const Settings &settings) : _wrapped(path, settings) { } void Database::open(EncryptionKey key, FnMut done) { _wrapped.with([ key, done = std::move(done) ](Implementation &unwrapped) mutable { unwrapped.open(key, std::move(done)); }); } void Database::close(FnMut done) { _wrapped.with([ done = std::move(done) ](Implementation &unwrapped) mutable { unwrapped.close(std::move(done)); }); } void Database::put( const Key &key, QByteArray value, FnMut done) { _wrapped.with([ key, value = std::move(value), done = std::move(done) ](Implementation &unwrapped) mutable { unwrapped.put(key, std::move(value), std::move(done)); }); } void Database::get(const Key &key, FnMut done) { _wrapped.with([ key, done = std::move(done) ](Implementation &unwrapped) mutable { unwrapped.get(key, std::move(done)); }); } void Database::remove(const Key &key, FnMut done) { _wrapped.with([ key, done = std::move(done) ](Implementation &unwrapped) mutable { unwrapped.remove(key, std::move(done)); }); } void Database::clear(FnMut done) { _wrapped.with([ done = std::move(done) ](Implementation &unwrapped) mutable { unwrapped.clear(std::move(done)); }); } Database::~Database() = default; } // namespace Cache } // namespace Storage