Pārlūkot izejas kodu

Use fileHash to avoid re-upload of already uploaded files

Neeraj Gupta 2 gadi atpakaļ
vecāks
revīzija
1f7c4c3b9f

+ 28 - 0
lib/db/files_db.dart

@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:flutter/foundation.dart';
 import 'package:logging/logging.dart';
 import 'package:path/path.dart';
 import 'package:path_provider/path_provider.dart';
@@ -814,6 +815,24 @@ class FilesDB {
     }
   }
 
+  Future<List<File>> getUploadedFilesWithHashes(
+    List<String> hash,
+    FileType fileType,
+    int ownerID,
+  ) async {
+    final db = await instance.database;
+    String rawQuery = 'SELECT * from files where ($columnUploadedFileID != '
+        'NULL OR $columnUploadedFileID != -1) AND $columnOwnerID = $ownerID '
+        'AND ($columnHash = "${hash.first}" OR $columnHash = "${hash.last}")';
+    final rows = await db.rawQuery(rawQuery, []);
+    if (rows.isNotEmpty) {
+      return _convertToFiles(rows);
+    } else {
+      debugPrint(rawQuery);
+      return [];
+    }
+  }
+
   Future<int> update(File file) async {
     final db = await instance.database;
     return await db.update(
@@ -843,6 +862,15 @@ class FilesDB {
     );
   }
 
+  Future<int> deleteByGeneratedID(int genID) async {
+    final db = await instance.database;
+    return db.delete(
+      table,
+      where: '$columnGeneratedID =?',
+      whereArgs: [genID],
+    );
+  }
+
   Future<int> deleteMultipleUploadedFiles(List<int> uploadedFileIDs) async {
     final db = await instance.database;
     return await db.delete(

+ 42 - 0
lib/services/collections_service.dart

@@ -671,6 +671,48 @@ class CollectionsService {
     }
   }
 
+  Future<void> linkLocalFileToExistingUploadedFileInAnotherCollection(
+    int destCollectionID,
+    File localFileToUpload,
+    File file,
+  ) async {
+    final params = <String, dynamic>{};
+    params["collectionID"] = destCollectionID;
+    params["files"] = [];
+
+    final key = decryptFileKey(file);
+    file.generatedID = localFileToUpload.generatedID; // So that a new entry is
+    // created in the FilesDB
+    file.localID = localFileToUpload.localID;
+    file.collectionID = destCollectionID;
+    final encryptedKeyData =
+        CryptoUtil.encryptSync(key, getCollectionKey(destCollectionID));
+    file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData);
+    file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce);
+
+    params["files"].add(
+      CollectionFileItem(
+        file.uploadedFileID,
+        file.encryptedKey,
+        file.keyDecryptionNonce,
+      ).toMap(),
+    );
+
+    try {
+      await _dio.post(
+        Configuration.instance.getHttpEndpoint() + "/collections/add-files",
+        data: params,
+        options: Options(
+          headers: {"X-Auth-Token": Configuration.instance.getToken()},
+        ),
+      );
+      await _filesDB.insertMultiple([file]);
+      Bus.instance.fire(CollectionUpdatedEvent(destCollectionID, [file]));
+    } catch (e) {
+      rethrow;
+    }
+  }
+
   Future<void> restore(int toCollectionID, List<File> files) async {
     final params = <String, dynamic>{};
     params["collectionID"] = toCollectionID;

+ 1 - 1
lib/ui/viewer/file/file_icons_widget.dart

@@ -8,7 +8,7 @@ class ThumbnailPlaceHolder extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    debugPrint("building placeHolder for thumbnail");
+    // debugPrint("building placeHolder for thumbnail");
     return Container(
       alignment: Alignment.center,
       color: Theme.of(context).colorScheme.galleryThumbBackgroundColor,

+ 139 - 15
lib/utils/file_uploader.dart

@@ -309,6 +309,7 @@ class FileUploader {
           rethrow;
         }
       }
+
       Uint8List key;
       bool isUpdatedFile =
           file.uploadedFileID != null && file.updationTime == -1;
@@ -317,6 +318,19 @@ class FileUploader {
         key = decryptFileKey(file);
       } else {
         key = null;
+        // check if the file is already uploaded and can be mapping to existing
+        // stuff
+        final isMappedToExistingUpload = await _mapToExistingUploadWithSameHash(
+          mediaUploadData,
+          file,
+          collectionID,
+        );
+        if (isMappedToExistingUpload) {
+          debugPrint(
+            "File success mapped to existing uploaded ${file.toString()}",
+          );
+          return file;
+        }
       }
 
       if (io.File(encryptedFilePath).existsSync()) {
@@ -422,23 +436,133 @@ class FileUploader {
       }
       rethrow;
     } finally {
-      if (mediaUploadData != null && mediaUploadData.sourceFile != null) {
-        // delete the file from app's internal cache if it was copied to app
-        // for upload. Shared Media should only be cleared when the upload
-        // succeeds.
-        if (io.Platform.isIOS ||
-            (uploadCompleted && file.isSharedMediaToAppSandbox())) {
-          await mediaUploadData.sourceFile.delete();
-        }
-      }
-      if (io.File(encryptedFilePath).existsSync()) {
-        await io.File(encryptedFilePath).delete();
-      }
-      if (io.File(encryptedThumbnailPath).existsSync()) {
-        await io.File(encryptedThumbnailPath).delete();
+      await _onUploadDone(
+        mediaUploadData,
+        uploadCompleted,
+        file,
+        encryptedFilePath,
+        encryptedThumbnailPath,
+      );
+    }
+  }
+
+  /*
+  // _mapToExistingUpload links the current file to be uploaded with the
+  // existing files. If the link is successful, it returns true other false.
+   When false, we should go ahead and re-upload or update the file
+    It performs following checks:
+    a) Uploaded file with same localID and destination collection. Delete the
+     fileToUpload entry
+    b) Uploaded file in destination collection but with missing localID.
+     Update the localID for uploadedFile and delete the fileToUpload entry
+    c) A uploaded file exist with same localID but in a different collection.
+    or
+    d) Uploaded file in different collection but missing localID.
+    For both c and d, perform add to collection operation.
+    e) File already exists but different localID. Re-upload
+    In case the existing files already have local identifier, which is
+    different from the {fileToUpload}, then most probably device has
+    duplicate files.
+  */
+  Future<bool> _mapToExistingUploadWithSameHash(
+    MediaUploadData mediaUploadData,
+    File fileToUpload,
+    int toCollectionID,
+  ) async {
+    List<String> hash = [mediaUploadData.fileHash];
+    if (fileToUpload.fileType == FileType.livePhoto) {
+      hash.add(mediaUploadData.zipHash);
+    }
+    List<File> existingFiles =
+        await FilesDB.instance.getUploadedFilesWithHashes(
+      hash,
+      fileToUpload.fileType,
+      Configuration.instance.getUserID(),
+    );
+    if (existingFiles?.isEmpty ?? true) {
+      return false;
+    } else {
+      debugPrint("Found some matches");
+    }
+    File fileSameLocalAndSameCollection = existingFiles.firstWhere(
+      (element) =>
+          element.uploadedFileID != -1 &&
+          element.collectionID == toCollectionID &&
+          element.localID == fileToUpload.localID,
+      orElse: () => null,
+    );
+    if (fileSameLocalAndSameCollection != null) {
+      debugPrint(
+        "fileSameLocalAndSameCollection: \n toUpload  ${fileToUpload.tag()} "
+        "\n existing: ${fileSameLocalAndSameCollection.tag()}",
+      );
+      // should delete the fileToUploadEntry
+      FilesDB.instance.deleteByGeneratedID(fileToUpload.generatedID);
+      return true;
+    }
+
+    File fileMissingLocalButSameCollection = existingFiles.firstWhere(
+      (element) =>
+          element.uploadedFileID != -1 &&
+          element.collectionID == toCollectionID &&
+          element.localID == null,
+      orElse: () => null,
+    );
+    if (fileMissingLocalButSameCollection != null) {
+      // update the local id of the existing file and delete the fileToUpload
+      // entry
+      debugPrint(
+        "fileMissingLocalButSameCollection: \n toUpload  ${fileToUpload.tag()} "
+        "\n existing: ${fileMissingLocalButSameCollection.tag()}",
+      );
+      fileMissingLocalButSameCollection.localID = fileToUpload.localID;
+      await FilesDB.instance.insert(fileMissingLocalButSameCollection);
+      await FilesDB.instance.deleteByGeneratedID(fileToUpload.generatedID);
+      return true;
+    }
+
+    File fileExistsButDifferentCollection = existingFiles.firstWhere(
+      (element) =>
+          element.uploadedFileID != -1 &&
+          element.collectionID != toCollectionID,
+      orElse: () => null,
+    );
+    if (fileExistsButDifferentCollection != null) {
+      debugPrint(
+        "fileExistsButDifferentCollection: \n toUpload  ${fileToUpload.tag()} "
+        "\n existing: ${fileExistsButDifferentCollection.tag()}",
+      );
+      await CollectionsService.instance
+          .linkLocalFileToExistingUploadedFileInAnotherCollection(
+              toCollectionID, fileToUpload, fileExistsButDifferentCollection);
+      return true;
+    }
+    return false;
+  }
+
+  Future<void> _onUploadDone(
+    MediaUploadData mediaUploadData,
+    bool uploadCompleted,
+    File file,
+    String encryptedFilePath,
+    String encryptedThumbnailPath,
+  ) async {
+    if (mediaUploadData != null && mediaUploadData.sourceFile != null) {
+      // delete the file from app's internal cache if it was copied to app
+      // for upload. Shared Media should only be cleared when the upload
+      // succeeds.
+      if (io.Platform.isIOS ||
+          (uploadCompleted && file.isSharedMediaToAppSandbox())) {
+        await mediaUploadData.sourceFile.delete();
       }
-      await _uploadLocks.releaseLock(file.localID, _processType.toString());
     }
+    if (io.File(encryptedFilePath).existsSync()) {
+      await io.File(encryptedFilePath).delete();
+    }
+    if (io.File(encryptedThumbnailPath).existsSync()) {
+      await io.File(encryptedThumbnailPath).delete();
+    }
+    await _uploadLocks.releaseLock(file.localID, _processType.toString());
   }
 
   Future _onInvalidFileError(File file, InvalidFileError e) async {