[mob] Use remote db for collections

This commit is contained in:
Neeraj Gupta
2025-02-14 15:24:57 +05:30
parent 6e85d24286
commit 2a1f2aded1
6 changed files with 202 additions and 99 deletions

View File

@@ -10,6 +10,7 @@ import "package:photos/models/collection/collection_old.dart";
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_migration/sqflite_migration.dart';
@Deprecated("Use remoteDB instead")
class CollectionsDB {
static const _databaseName = "ente.collections.db";
static const table = 'collections';

View File

@@ -1,32 +1,95 @@
import "dart:developer";
import "dart:io";
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
import "package:path/path.dart";
import "package:path_provider/path_provider.dart";
import "package:photos/db/remote/migration.dart";
import "package:photos/models/collection/collection.dart";
import "package:sqlite_async/sqlite_async.dart";
var devLog = log;
// ignore: constant_identifier_names
enum RemoteTable { collections, collection_files, files, entities }
class RemoteDB {
static const _databaseName = "remote.db";
// only have a single app-wide reference to the database
static Future<SqliteDatabase>? _sqliteAsyncDBFuture;
static const _batchInsertMaxCount = 1000;
late final SqliteDatabase _sqliteDB;
Future<SqliteDatabase> get sqliteAsyncDB async {
// lazily instantiate the db the first time it is accessed
_sqliteAsyncDBFuture ??= _initSqliteAsyncDatabase();
return _sqliteAsyncDBFuture!;
}
// this opens the database (and creates it if it doesn't exist)
Future<SqliteDatabase> _initSqliteAsyncDatabase() async {
Future<void> init() async {
devLog("Starting RemoteDB init");
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, _databaseName);
devLog("DB path " + path);
final database = SqliteDatabase(path: path);
await RemoteDBMigration.migrate(database);
return database;
_sqliteDB = database;
devLog("RemoteDB init complete $path");
}
Future<List<Collection>> getAllCollections() async {
final result = <Collection>[];
final cursor = await _sqliteDB.getAll("SELECT * FROM collections");
for (final row in cursor) {
result.add(Collection.fromRow(row));
}
return result;
}
Future<void> insertCollections(List<Collection> collections) async {
if (collections.isEmpty) return;
final stopwatch = Stopwatch()..start();
int inserted = 0;
await Future.forEach(collections.slices(_batchInsertMaxCount),
(slice) async {
debugPrint(
'$runtimeType insertCollections inserting slice of [${inserted + 1}, ${inserted + slice.length}] entries',
);
final List<List<Object?>> values =
slice.map((e) => e.rowValiues()).toList();
await _sqliteDB.executeBatch(
'INSERT OR REPLACE INTO collections ($collectionColumns) values($collectionValuePlaceHolder)',
values,
);
inserted += slice.length;
});
debugPrint(
'$runtimeType insertCollections complete in ${stopwatch.elapsed.inMilliseconds}ms for ${collections.length} collections',
);
}
Future<void> deleteEntries<T>(Set<T> ids, RemoteTable table) async {
if (ids.isEmpty) return;
final stopwatch = Stopwatch()..start();
await _sqliteDB.execute(
'DELETE FROM ${table.name.toLowerCase()} WHERE id IN (${ids.join(',')})',
);
debugPrint(
'$runtimeType deleteEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${ids.length} $table entries',
);
}
Future<Set<T>> _getByIds<T>(
Set<int> ids,
String table,
T Function(
Map<String, Object?> row,
) mapRow, {
String columnName = "id",
}) async {
final result = <T>{};
if (ids.isNotEmpty) {
final rows = await _sqliteDB.getAll(
'SELECT * from $table where $columnName IN (${ids.join(',')})',
);
for (final row in rows) {
result.add(mapRow(row));
}
}
return result;
}
}

View File

@@ -1,6 +1,15 @@
import "package:flutter/cupertino.dart";
import "package:sqlite_async/sqlite_async.dart";
const collectionColumns =
'id, owner, enc_key, enc_key_nonce, name, type, local_path, '
'is_deleted, updation_time, sharees, public_urls, mmd_encoded_json, '
'mmd_ver, pub_mmd_encoded_json, pub_mmd_ver, shared_mmd_json, '
'shared_mmd_ver';
String collectionValuePlaceHolder =
collectionColumns.split(',').map((_) => '?').join(',');
class RemoteDBMigration {
static const migrationScripts = [
'''
@@ -12,31 +21,31 @@ class RemoteDBMigration {
name TEXT NOT NULL,
type TEXT NOT NULL,
local_path TEXT,
is_deleted INTEGER NOT NULL
is_deleted INTEGER NOT NULL,
updation_time INTEGER NOT NULL,
sharees TEXT NOT NULL DEFAULT [],
public_urls TEXT NOT NULL DEFAULT [],
mmd_encoded_json TEXT NOT NULL DEFAULT {},
sharees TEXT NOT NULL DEFAULT '[]',
public_urls TEXT NOT NULL DEFAULT '[]',
mmd_encoded_json TEXT NOT NULL DEFAULT '{}',
mmd_ver INTEGER NOT NULL DEFAULT 0,
pub_mmd_encoded_json TEXT DEFAULT {}',
pub_mmd_encoded_json TEXT DEFAULT '{}',
pub_mmd_ver INTEGER NOT NULL DEFAULT 0,
shared_mmd_json TEXT NOT NULL {},
shared_mmd_ver INTEGER NOT NULL DEFAULT 0,
)
''',
'''
CREATE TABLE collection_files (
file_id INTEGER NOT NULL,
collection_id INTEGER NOT NULL,
PRIMARY KEY (file_id, collection_id)
enc_key TEXT NOT NULL,
enc_key_nonce TEXT NOT NULL,
is_deleted INTEGER NOT NULL
updated_at INTEGER NOT NULL,
created_at INTEGER NOT NULL DEFAULT 0,
)
shared_mmd_json TEXT NOT NULL DEFAULT '{}',
shared_mmd_ver INTEGER NOT NULL DEFAULT 0
);
''',
];
// '''
// CREATE TABLE collection_files (
// file_id INTEGER NOT NULL,
// collection_id INTEGER NOT NULL,
// PRIMARY KEY (file_id, collection_id)
// enc_key TEXT NOT NULL,
// enc_key_nonce TEXT NOT NULL,
// is_deleted INTEGER NOT NULL
// updated_at INTEGER NOT NULL,
// created_at INTEGER NOT NULL DEFAULT 0,
// )
// ''',
static Future<void> migrate(
SqliteDatabase database,

View File

@@ -238,15 +238,13 @@ class Collection {
final sharees = List<User>.from(
(json.decode(map['sharees']) as List).map((x) => User.fromMap(x)),
);
final List<PublicURL> publicURLs = map['public_urls'] == null
? []
: List<PublicURL>.from(
(json.decode(map['public_urls']) as List)
.map((x) => PublicURL.fromMap(x)),
);
final List<PublicURL> publicURLs = List<PublicURL>.from(
(json.decode(map['public_urls']) as List)
.map((x) => PublicURL.fromMap(x)),
);
return Collection(
id: map['id'],
owner: User.fromMap(map['owner']),
owner: User.fromJson(map['owner']),
encryptedKey: map['enc_key'],
keyDecryptionNonce: map['enc_key_nonce'],
name: map['name'],
@@ -255,36 +253,36 @@ class Collection {
publicURLs: publicURLs,
updationTime: map['updation_time'],
localPath: map['local_path'],
isDeleted: map['isDeleted'] ?? false,
mMdEncodedJson: map['mmd_encoded_json'] ?? '{}',
mMdVersion: map['mmd_ver'] ?? 0,
mMdPubEncodedJson: map['pub_mmd_encoded_json'] ?? '{}',
mMbPubVersion: map['pub_mmd_ver'] ?? 0,
sharedMmdJson: map['shared_mmd_json'] ?? '{}',
sharedMmdVersion: map['shared_mmd_ver'] ?? 0,
isDeleted: (map['is_deleted'] as int) == 1,
mMdEncodedJson: map['mmd_encoded_json'],
mMdVersion: map['mmd_ver'],
mMdPubEncodedJson: map['pub_mmd_encoded_json'],
mMbPubVersion: map['pub_mmd_ver'],
sharedMmdJson: map['shared_mmd_json'],
sharedMmdVersion: map['shared_mmd_ver'],
);
}
static Map<String, dynamic> toRow(Collection c) {
return {
'id': c.id,
'owner': c.owner.toMap(),
'enc_key': c.encryptedKey,
'enc_key_nonce': c.keyDecryptionNonce,
'name': c.name,
'type': typeToString(c.type),
'sharees': json.encode(c.sharees.map((x) => x.toMap()).toList()),
'public_urls': json.encode(c.publicURLs.map((x) => x.toMap()).toList()),
'updation_time': c.updationTime,
'local_path': c.localPath,
'isDeleted': c.isDeleted,
'mmd_encoded_json': c.mMdEncodedJson,
'mmd_ver': c.mMdVersion,
'pub_mmd_encoded_json': c.mMdPubEncodedJson,
'pub_mmd_ver': c.mMbPubVersion,
'shared_mmd_json': c.sharedMmdJson,
'shared_mmd_ver': c.sharedMmdVersion,
};
List<Object?> rowValiues() {
return [
id,
owner.toJson(),
encryptedKey,
keyDecryptionNonce,
name,
typeToString(type),
localPath,
isDeleted ? 1 : 0,
updationTime,
json.encode(sharees.map((x) => x.toMap()).toList()),
json.encode(publicURLs.map((x) => x.toMap()).toList()),
mMdEncodedJson,
mMdVersion,
mMdPubEncodedJson,
mMbPubVersion,
sharedMmdJson,
sharedMmdVersion,
];
}
}

View File

@@ -3,6 +3,7 @@ import "package:ente_cast/ente_cast.dart";
import "package:ente_cast_normal/ente_cast_normal.dart";
import "package:ente_feature_flag/ente_feature_flag.dart";
import "package:package_info_plus/package_info_plus.dart";
import "package:photos/db/remote/db.dart";
import "package:photos/gateways/entity_gw.dart";
import "package:photos/services/billing_service.dart";
import "package:photos/services/entity_service.dart";
@@ -137,3 +138,9 @@ FaceRecognitionService get faceRecognitionService {
_faceRecognitionService ??= FaceRecognitionService();
return _faceRecognitionService!;
}
RemoteDB? _remoteDB;
RemoteDB get remoteDB {
_remoteDB ??= RemoteDB();
return _remoteDB!;
}

View File

@@ -16,6 +16,7 @@ import 'package:photos/core/network/network.dart';
import 'package:photos/db/collections_db.dart';
import 'package:photos/db/device_files_db.dart';
import 'package:photos/db/files_db.dart';
import "package:photos/db/remote/db.dart";
import 'package:photos/db/trash_db.dart';
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/files_updated_event.dart';
@@ -47,8 +48,8 @@ import "package:photos/utils/local_settings.dart";
import 'package:shared_preferences/shared_preferences.dart';
class CollectionsService {
static const _collectionSyncTimeKeyPrefix = "collection_sync_time_";
static const _collectionsSyncTimeKey = "collections_sync_time_x";
static const _collectionSyncTimeKeyPrefix = "collection_sync_time_x";
static const _collectionsSyncTimeKey = "collections_sync_time_xx";
static const int kMaximumWriteAttempts = 5;
@@ -87,13 +88,25 @@ class CollectionsService {
Future<void> init(SharedPreferences preferences) async {
_prefs = preferences;
final collections = await _db.getAllCollections();
await remoteDB.init();
final collections = await _db.getAllCollections();
final List<Collection> collectionsToInsert = [];
for (final collection in collections) {
// using deprecated method because the path is stored in encrypted
// format in the DB
_cacheCollectionAttributes(collection);
final r = _cacheCollectionAttributes(collection);
collectionsToInsert.add(r);
}
await remoteDB.insertCollections(collectionsToInsert);
await _db.clearTable();
if (collectionsToInsert.isEmpty) {
final newColections = await remoteDB.getAllCollections();
for (final collection in newColections) {
_cacheLocalPathAndCollection(collection);
}
}
Bus.instance.on<CollectionUpdatedEvent>().listen((event) {
_collectionIDToNewestFileTime = null;
if (event.collectionID != null) {
@@ -131,6 +144,7 @@ class CollectionsService {
int maxUpdationTime = lastCollectionUpdationTime;
final ownerID = _config.getUserID();
bool shouldFireDeleteEvent = false;
final Set<int> toDelete = {};
for (final collection in fetchedCollections) {
if (collection.isDeleted) {
await _filesDB.deleteCollection(collection.id);
@@ -141,7 +155,7 @@ class CollectionsService {
}
// remove reference for incoming collections when unshared/deleted
if (collection.isDeleted && ownerID != collection.owner.id) {
await _db.deleteCollection(collection.id);
toDelete.add(collection.id);
} else {
// keep entry for deletedCollection as collectionKey may be used during
// trash file decryption
@@ -159,6 +173,9 @@ class CollectionsService {
),
);
}
if (toDelete.isNotEmpty) {
await remoteDB.deleteEntries(toDelete, RemoteTable.collections);
}
await _updateDB(updatedCollections);
await _prefs.setInt(_collectionsSyncTimeKey, maxUpdationTime);
watch.logAndReset("till DB insertion ${updatedCollections.length}");
@@ -191,8 +208,12 @@ class CollectionsService {
}
Future<Map<int, int>> getCollectionIDsToBeSynced() async {
final idsToRemoveUpdateTimeMap =
await _db.getActiveIDsAndRemoteUpdateTime();
final idsToRemoveUpdateTimeMap = <int, int>{};
for (final collection in _collectionIDToCollections.values) {
if (!collection.isDeleted) {
idsToRemoveUpdateTimeMap[collection.id] = collection.updationTime;
}
}
final result = <int, int>{};
for (final MapEntry<int, int> e in idsToRemoveUpdateTimeMap.entries) {
final int cid = e.key;
@@ -551,7 +572,9 @@ class CollectionsService {
}
_collectionIDToCollections[collectionID] =
_collectionIDToCollections[collectionID]!.copyWith(sharees: sharees);
unawaited(_db.insert([_collectionIDToCollections[collectionID]!]));
unawaited(
_updateDB([_collectionIDToCollections[collectionID]!]),
);
RemoteSyncService.instance.sync(silently: true).ignore();
return sharees;
} on DioException catch (e) {
@@ -577,7 +600,9 @@ class CollectionsService {
}
_collectionIDToCollections[collectionID] =
_collectionIDToCollections[collectionID]!.copyWith(sharees: sharees);
unawaited(_db.insert([_collectionIDToCollections[collectionID]!]));
unawaited(
_updateDB([_collectionIDToCollections[collectionID]!]),
);
RemoteSyncService.instance.sync(silently: true).ignore();
return sharees;
} catch (e) {
@@ -638,7 +663,7 @@ class CollectionsService {
if (isBulkDelete) {
final deletedCollection = collection.copyWith(isDeleted: true);
_collectionIDToCollections[collection.id] = deletedCollection;
unawaited(_db.insert([deletedCollection]));
unawaited(_updateDB([deletedCollection]));
} else {
await _handleCollectionDeletion(collection);
}
@@ -656,7 +681,7 @@ class CollectionsService {
Future<void> _handleCollectionDeletion(Collection collection) async {
await _filesDB.deleteCollection(collection.id);
final deletedCollection = collection.copyWith(isDeleted: true);
unawaited(_db.insert([deletedCollection]));
unawaited(_updateDB([deletedCollection]));
_collectionIDToCollections[collection.id] = deletedCollection;
Bus.instance.fire(
CollectionUpdatedEvent(
@@ -962,7 +987,7 @@ class CollectionsService {
},
);
collection.publicURLs.add(PublicURL.fromMap(response.data["result"]));
await _db.insert(List.from([collection]));
await _updateDB(List.from([collection]));
_collectionIDToCollections[collection.id] = collection;
Bus.instance.fire(
CollectionUpdatedEvent(collection.id, <EnteFile>[], "shareUrL"),
@@ -991,7 +1016,7 @@ class CollectionsService {
// remove existing url information
collection.publicURLs.clear();
collection.publicURLs.add(PublicURL.fromMap(response.data["result"]));
await _db.insert(List.from([collection]));
await _updateDB(List.from([collection]));
_collectionIDToCollections[collection.id] = collection;
Bus.instance.fire(
CollectionUpdatedEvent(collection.id, <EnteFile>[], "updateUrl"),
@@ -1013,7 +1038,7 @@ class CollectionsService {
"/collections/share-url/" + collection.id.toString(),
);
collection.publicURLs.clear();
await _db.insert(List.from([collection]));
await _updateDB(List.from([collection]));
_collectionIDToCollections[collection.id] = collection;
Bus.instance.fire(
CollectionUpdatedEvent(
@@ -1325,7 +1350,7 @@ class CollectionsService {
assert(response.data != null);
final collectionData = response.data["collection"];
final collection = await _fromRemoteCollection(collectionData);
await _db.insert(List.from([collection]));
await _updateDB(List.from([collection]));
_cacheLocalPathAndCollection(collection);
return collection;
} catch (e) {
@@ -1902,19 +1927,6 @@ class CollectionsService {
});
}
@Deprecated("Use _cacheLocalPathAndCollection instead")
CollectionV2 _cacheCollectionAttributes(CollectionV2 collection) {
final String decryptedName = _getDecryptedCollectionName(collection);
collection.setName(decryptedName);
if (collection.canLinkToDevicePath(_config.getUserID()!)) {
collection.decryptedPath = _decryptCollectionPath(collection);
_localPathToCollectionID[collection.decryptedPath!] = collection.id;
}
final Collection c = Collection.fromOldCollection(collection);
_collectionIDToCollections[collection.id] = c;
return collection;
}
Collection _cacheLocalPathAndCollection(Collection collection) {
assert(
collection.name != null,
@@ -1927,6 +1939,23 @@ class CollectionsService {
return collection;
}
bool hasSyncedCollections() {
return _prefs.containsKey(_collectionsSyncTimeKey);
}
@Deprecated("Use _cacheLocalPathAndCollection instead")
Collection _cacheCollectionAttributes(CollectionV2 collection) {
final String decryptedName = _getDecryptedCollectionName(collection);
collection.setName(decryptedName);
if (collection.canLinkToDevicePath(_config.getUserID()!)) {
collection.decryptedPath = _decryptCollectionPath(collection);
_localPathToCollectionID[collection.decryptedPath!] = collection.id;
}
final Collection c = Collection.fromOldCollection(collection);
_collectionIDToCollections[collection.id] = c;
return c;
}
String _decryptCollectionPath(CollectionV2 collection) {
final existingPath = collection.decryptedPath;
if (existingPath != null && existingPath.isNotEmpty) {
@@ -1950,10 +1979,6 @@ class CollectionsService {
);
}
bool hasSyncedCollections() {
return _prefs.containsKey(_collectionsSyncTimeKey);
}
String _getDecryptedCollectionName(CollectionV2 collection) {
if (collection.isDeleted) {
return "Deleted Album";
@@ -1990,7 +2015,7 @@ class CollectionsService {
return;
}
try {
await _db.insert(collections);
await remoteDB.insertCollections(collections);
} catch (e) {
_logger.severe("Failed to update collections", e);
if (attempt < kMaximumWriteAttempts) {