trash_db.dart 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'package:logging/logging.dart';
  4. import 'package:path/path.dart';
  5. import 'package:path_provider/path_provider.dart';
  6. import 'package:photos/models/file_load_result.dart';
  7. import 'package:photos/models/trash_file.dart';
  8. import 'package:sqflite/sqflite.dart';
  9. // The TrashDB doesn't need to flatten and store all attributes of a file.
  10. // Before adding any other column, we should evaluate if we need to query on that
  11. // column or not while showing trashed items. Even if we miss storing any new attributes,
  12. // during restore, all file attributes will be fetched & stored as required.
  13. class TrashDB {
  14. static const _databaseName = "ente.trash.db";
  15. static const _databaseVersion = 1;
  16. static final Logger _logger = Logger("TrashDB");
  17. static const tableName = 'trash';
  18. static const columnUploadedFileID = 'uploaded_file_id';
  19. static const columnCollectionID = 'collection_id';
  20. static const columnOwnerID = 'owner_id';
  21. static const columnTrashUpdatedAt = 't_updated_at';
  22. static const columnTrashDeleteBy = 't_delete_by';
  23. static const columnEncryptedKey = 'encrypted_key';
  24. static const columnKeyDecryptionNonce = 'key_decryption_nonce';
  25. static const columnFileDecryptionHeader = 'file_decryption_header';
  26. static const columnThumbnailDecryptionHeader = 'thumbnail_decryption_header';
  27. static const columnUpdationTime = 'updation_time';
  28. static const columnCreationTime = 'creation_time';
  29. static const columnLocalID = 'local_id';
  30. // standard file metadata, which isn't editable
  31. static const columnFileMetadata = 'file_metadata';
  32. static const columnMMdEncodedJson = 'mmd_encoded_json';
  33. static const columnMMdVersion = 'mmd_ver';
  34. static const columnPubMMdEncodedJson = 'pub_mmd_encoded_json';
  35. static const columnPubMMdVersion = 'pub_mmd_ver';
  36. Future _onCreate(Database db, int version) async {
  37. await db.execute(
  38. '''
  39. CREATE TABLE $tableName (
  40. $columnUploadedFileID INTEGER PRIMARY KEY NOT NULL,
  41. $columnCollectionID INTEGER NOT NULL,
  42. $columnOwnerID INTEGER,
  43. $columnTrashUpdatedAt INTEGER NOT NULL,
  44. $columnTrashDeleteBy INTEGER NOT NULL,
  45. $columnEncryptedKey TEXT,
  46. $columnKeyDecryptionNonce TEXT,
  47. $columnFileDecryptionHeader TEXT,
  48. $columnThumbnailDecryptionHeader TEXT,
  49. $columnUpdationTime INTEGER,
  50. $columnLocalID TEXT,
  51. $columnCreationTime INTEGER NOT NULL,
  52. $columnFileMetadata TEXT DEFAULT '{}',
  53. $columnMMdEncodedJson TEXT DEFAULT '{}',
  54. $columnMMdVersion INTEGER DEFAULT 0,
  55. $columnPubMMdEncodedJson TEXT DEFAULT '{}',
  56. $columnPubMMdVersion INTEGER DEFAULT 0
  57. );
  58. CREATE INDEX IF NOT EXISTS creation_time_index ON $tableName($columnCreationTime);
  59. CREATE INDEX IF NOT EXISTS delete_by_time_index ON $tableName($columnTrashDeleteBy);
  60. CREATE INDEX IF NOT EXISTS updated_at_time_index ON $tableName($columnTrashUpdatedAt);
  61. ''',
  62. );
  63. }
  64. TrashDB._privateConstructor();
  65. static final TrashDB instance = TrashDB._privateConstructor();
  66. // only have a single app-wide reference to the database
  67. static Future<Database> _dbFuture;
  68. Future<Database> get database async {
  69. // lazily instantiate the db the first time it is accessed
  70. _dbFuture ??= _initDatabase();
  71. return _dbFuture;
  72. }
  73. // this opens the database (and creates it if it doesn't exist)
  74. Future<Database> _initDatabase() async {
  75. final Directory documentsDirectory = await getApplicationDocumentsDirectory();
  76. final String path = join(documentsDirectory.path, _databaseName);
  77. _logger.info("DB path " + path);
  78. return await openDatabase(
  79. path,
  80. version: _databaseVersion,
  81. onCreate: _onCreate,
  82. );
  83. }
  84. Future<void> clearTable() async {
  85. final db = await instance.database;
  86. await db.delete(tableName);
  87. }
  88. // getRecentlyTrashedFile returns the file which was trashed recently
  89. Future<TrashFile> getRecentlyTrashedFile() async {
  90. final db = await instance.database;
  91. final rows = await db.query(
  92. tableName,
  93. orderBy: '$columnTrashDeleteBy DESC',
  94. limit: 1,
  95. );
  96. if (rows == null || rows.isEmpty) {
  97. return null;
  98. }
  99. return _getTrashFromRow(rows[0]);
  100. }
  101. Future<int> count() async {
  102. final db = await instance.database;
  103. final count = Sqflite.firstIntValue(
  104. await db.rawQuery('SELECT COUNT(*) FROM $tableName'),
  105. );
  106. return count;
  107. }
  108. Future<void> insertMultiple(List<TrashFile> trashFiles) async {
  109. final startTime = DateTime.now();
  110. final db = await instance.database;
  111. var batch = db.batch();
  112. int batchCounter = 0;
  113. for (TrashFile trash in trashFiles) {
  114. if (batchCounter == 400) {
  115. await batch.commit(noResult: true);
  116. batch = db.batch();
  117. batchCounter = 0;
  118. }
  119. batch.insert(
  120. tableName,
  121. _getRowForTrash(trash),
  122. conflictAlgorithm: ConflictAlgorithm.replace,
  123. );
  124. batchCounter++;
  125. }
  126. await batch.commit(noResult: true);
  127. final endTime = DateTime.now();
  128. final duration = Duration(
  129. microseconds:
  130. endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch,
  131. );
  132. _logger.info(
  133. "Batch insert of " +
  134. trashFiles.length.toString() +
  135. " took " +
  136. duration.inMilliseconds.toString() +
  137. "ms.",
  138. );
  139. }
  140. Future<int> insert(TrashFile trash) async {
  141. final db = await instance.database;
  142. return db.insert(
  143. tableName,
  144. _getRowForTrash(trash),
  145. conflictAlgorithm: ConflictAlgorithm.replace,
  146. );
  147. }
  148. Future<int> delete(List<int> uploadedFileIDs) async {
  149. final db = await instance.database;
  150. return db.delete(
  151. tableName,
  152. where: '$columnUploadedFileID IN (${uploadedFileIDs.join(', ')})',
  153. );
  154. }
  155. Future<int> update(TrashFile file) async {
  156. final db = await instance.database;
  157. return await db.update(
  158. tableName,
  159. _getRowForTrash(file),
  160. where: '$columnUploadedFileID = ?',
  161. whereArgs: [file.uploadedFileID],
  162. );
  163. }
  164. Future<FileLoadResult> getTrashedFiles(
  165. int startTime,
  166. int endTime, {
  167. int limit,
  168. bool asc,
  169. }) async {
  170. final db = await instance.database;
  171. final order = (asc ?? false ? 'ASC' : 'DESC');
  172. final results = await db.query(
  173. tableName,
  174. where: '$columnCreationTime >= ? AND $columnCreationTime <= ?',
  175. whereArgs: [startTime, endTime],
  176. orderBy: '$columnCreationTime ' + order,
  177. limit: limit,
  178. );
  179. final files = _convertToFiles(results);
  180. return FileLoadResult(files, files.length == limit);
  181. }
  182. List<TrashFile> _convertToFiles(List<Map<String, dynamic>> results) {
  183. final List<TrashFile> trashedFiles = [];
  184. for (final result in results) {
  185. trashedFiles.add(_getTrashFromRow(result));
  186. }
  187. return trashedFiles;
  188. }
  189. TrashFile _getTrashFromRow(Map<String, dynamic> row) {
  190. final trashFile = TrashFile();
  191. trashFile.updateAt = row[columnTrashUpdatedAt];
  192. trashFile.deleteBy = row[columnTrashDeleteBy];
  193. trashFile.uploadedFileID = row[columnUploadedFileID];
  194. // dirty hack to ensure that the file_downloads & cache mechanism works
  195. trashFile.generatedID = -1 * trashFile.uploadedFileID;
  196. trashFile.ownerID = row[columnOwnerID];
  197. trashFile.collectionID =
  198. row[columnCollectionID] == -1 ? null : row[columnCollectionID];
  199. trashFile.encryptedKey = row[columnEncryptedKey];
  200. trashFile.keyDecryptionNonce = row[columnKeyDecryptionNonce];
  201. trashFile.fileDecryptionHeader = row[columnFileDecryptionHeader];
  202. trashFile.thumbnailDecryptionHeader = row[columnThumbnailDecryptionHeader];
  203. trashFile.updationTime = row[columnUpdationTime] ?? 0;
  204. trashFile.localID = row[columnLocalID];
  205. trashFile.creationTime = row[columnCreationTime];
  206. final fileMetadata = row[columnFileMetadata] ?? '{}';
  207. trashFile.applyMetadata(jsonDecode(fileMetadata));
  208. trashFile.mMdVersion = row[columnMMdVersion] ?? 0;
  209. trashFile.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
  210. trashFile.pubMmdVersion = row[columnPubMMdVersion] ?? 0;
  211. trashFile.pubMmdEncodedJson = row[columnPubMMdEncodedJson] ?? '{}';
  212. if (trashFile.pubMagicMetadata != null &&
  213. trashFile.pubMagicMetadata.editedTime != null) {
  214. // override existing creationTime to avoid re-writing all queries related
  215. // to loading the gallery
  216. row[columnCreationTime] = trashFile.pubMagicMetadata.editedTime;
  217. }
  218. return trashFile;
  219. }
  220. Map<String, dynamic> _getRowForTrash(TrashFile trash) {
  221. final row = <String, dynamic>{};
  222. row[columnTrashUpdatedAt] = trash.updateAt;
  223. row[columnTrashDeleteBy] = trash.deleteBy;
  224. row[columnUploadedFileID] = trash.uploadedFileID;
  225. row[columnCollectionID] = trash.collectionID;
  226. row[columnOwnerID] = trash.ownerID;
  227. row[columnEncryptedKey] = trash.encryptedKey;
  228. row[columnKeyDecryptionNonce] = trash.keyDecryptionNonce;
  229. row[columnFileDecryptionHeader] = trash.fileDecryptionHeader;
  230. row[columnThumbnailDecryptionHeader] = trash.thumbnailDecryptionHeader;
  231. row[columnUpdationTime] = trash.updationTime;
  232. row[columnLocalID] = trash.localID;
  233. row[columnCreationTime] = trash.creationTime;
  234. row[columnFileMetadata] = jsonEncode(trash.getMetadata());
  235. row[columnMMdVersion] = trash.mMdVersion ?? 0;
  236. row[columnMMdEncodedJson] = trash.mMdEncodedJson ?? '{}';
  237. row[columnPubMMdVersion] = trash.pubMmdVersion ?? 0;
  238. row[columnPubMMdEncodedJson] = trash.pubMmdEncodedJson ?? '{}';
  239. return row;
  240. }
  241. }