منع رفع الملفات المكررة على Firebase Storage

كيفية منع رفع الملفات المكررة على Firebase Storage عبر MD5 Hashing

AbdulAlim Rajjoubمنذ 6 سنوات

سنشرح في هذا الدرس عن كيفية منع رفع الملفات المكررة الى Firebase Storage,هذا الأسلوب يستخدمه تطبيق الواتساب على سبيل المثال,فتجد أحياناً بأنه اذا قمت بإرسال صورة أو فيديو (متداولة بكثرة) الى أحد الأشخاص فإنه يتم إرسالها بسرعة وكأنك ترسل رسالة نصية,وذلك بسبب أنه عندما يتم رفع الصورة لأولى مرة على سيرفر الواتس أب فإنه يتم حفظ ال MD5 Hash الخاص بالملف.
 

ماهو MD5 Hash؟

الMD5 Hash بالنسبة للملفات يمكنك أن تعتبره ك ID خاص للملف,أي أنه يستحيل أن يتطابق ملفان بنفس MD5.

الMD5 لا يتغير اذا قمت بتغيير الإسم او الصيغة ولكن يتغير في حالة قمت بالتعديل على الملف (تغيير حجم الصورة مثلاً)

 

وبالتالي فإن الواتساب يقوم بالتحقق قبل رفع أي ملف على السيرفر ,اذا كان الملف موجود مسبقاً على السيرفر بنفس MD5 الموجود لدى Client فإنه بدل من رفع الملف يقوم فقط بحفظ مسار الملف في قاعدة البيانات ,وبالتالي تقليل استهلاك المساحة لدى السيرفر وتوفير البيانات لدى Client.

وهذا ماسنفعله في هذا الدرس

 

نقوم بإنشاء مشروع على Android Studio وعلى Firebase Console ونقوم بربطهم ببعضهم البعض

سنقوم باستخدام نفس الmethods التي استعملناها في درس أساسيات Firebase Storage  بالنسبة لاختيار ورفع الصور

public class MainActivity extends AppCompatActivity {

    FirebaseStorage firebaseStorage;
    StorageReference mainRef;
    FirebaseDatabase database;


    public static final int PICK_IMG_REQUEST = 6541;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        firebaseStorage = FirebaseStorage.getInstance();
        mainRef = firebaseStorage.getReference();
        database = FirebaseDatabase.getInstance();


        Button button = findViewById(R.id.upload_btn);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
                photoPickerIntent.setType("image/*");
                startActivityForResult(photoPickerIntent, PICK_IMG_REQUEST);
            }
        });
    }

 

أما الآن سنقوم بإنشاء Util Class نسميه MD5Util ونقوم بإنشاء ميثود مهمتها توليد MD5  لهذا للملف الذي سنرفعه 

لا يجب عليك فهم كل هذا الكود ,في النهاية ستقوم هذه الميثود بأخذ File كبارامتر وتقوم إما بإرحاع null في حالة وجود أي مشاكل او ستقوم بإرجاع MD5

public class MD5Util {
    private static final String TAG = "MD5";

    
    public static String calculateMD5(File file) {
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "Exception while getting digest", e);
            return null;
        }

        InputStream is;
        try {
            is = new FileInputStream(updateFile);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Exception while getting FileInputStream", e);
            return null;
        }

        byte[] buffer = new byte[8192];
        int read;
        try {
            while ((read = is.read(buffer)) > 0) {
                digest.update(buffer, 0, read);
            }
            byte[] md5sum = digest.digest();
            BigInteger bigInt = new BigInteger(1, md5sum);
            String output = bigInt.toString(16);
            // Fill to 32 chars
            output = String.format("%32s", output).replace(' ', '0');
            return output;
        } catch (IOException e) {
            throw new RuntimeException("Unable to process file for MD5", e);
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                Log.e(TAG, "Exception on closing MD5 input stream", e);
            }
        }
    }


}

الآن نعود ونقوم بأخذ الصورة التي تم اختيارها عبر onActivityResult

ونقوم بأخذ md5 للملف

 if (resultCode == RESULT_OK) {
            final Uri imageUri = data.getData();
            Log.d("3llomi", "IMAGE URI is " + imageUri);
            File imageFile = new File(getPath(imageUri));
            Log.d("3llomi", "image file path is " + imageFile.getPath());
            final String md5 = MD5Util.calculateMD5(imageFile);
}else {
            Toast.makeText(this, "no image selected :/", Toast.LENGTH_SHORT).show();
        }

ملاحظة:يجب عليك إعطاء الصلاحيات READ_EXTERNAL_STORAGE في AndroidManifest 

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

كما يتوجب عليك التحقق من Runtime Permissions اذا كان النظام Andoroid 6.0+ 

 

سنقوم بإنشاء الميثود upload التي تأخذ مسار الملف uri و md5 وتقوم برفع الملف الى Firebase Storage 

وعند نجاح عملية الرفع  نقوم بوضع md5 الملف ومساره في الداتابيز 

    private void upload(Uri uri, final String md5) {
        final String imageName = UUID.randomUUID().toString() + ".jpg";

        mainRef.child(imageName).putFile(uri)
                .addOnCompleteListener(new OnCompleteListener<UploadTask.TaskSnapshot>() {
                    @Override
                    public void onComplete(@android.support.annotation.NonNull Task<UploadTask.TaskSnapshot> task) {

                        if (task.isSuccessful()) {
                            String path = task.getResult().getStorage().getPath();
                            database.getReference().child("hash").child(md5).setValue(path);
                            Toast.makeText(MainActivity.this, "Uplaod Succeed", Toast.LENGTH_SHORT).show();

                        } else {
                            Log.d("3llomi", "upload Failed " + task.getException().getLocalizedMessage());
                            Toast.makeText(MainActivity.this, "Uplaod Failed :( " + task.getException().getLocalizedMessage(), Toast.LENGTH_LONG).show();
                        }
                    }
                });
    }

الآن نقوم بتشغيل التطبيق ونجرب رفع صورة ما وعند نجاح الرفع الصورة سنجد md5 والمسار في الداتابيز بهذا الشكل

الآن نعود للتطبيق ونقوم بالتحقق قبل الرفع من وجود الhash في الداتابيز أم لا, ولأنه قد نحتاج هذا التحقق في أكثر من Activity داخل التطبيق فسنقوم بإنشاء كلاس FireUtil ونضع به الميثود checkIfFileExists التي ستقوم بالتحقق فيما اذا كان hash موجود ام لا, اذا كان موجود عندها سنقوم بإحضار مسار الملف.

ولذلك سنقوم بإنشاء interface OnFinish سنقوم باستدعاءها عند الانتهاء من جلب البيانات من Database

 public interface OnFinish {
        void onFound(String path);

        void onNotFound();
    }

أما الآن لننشئ الميثود checkIfFileExists

كما نرى أنه في حالة كان md5 null فنعود ب onNotFound وإلا نقوم بالتحقق في Database اذا كان md5 موجود 

اذا كانت datasnapshot == null عندها فإن الملف لم يتم رفعه من قبل فسنعود ب onNotFound 

وإلا سنقوم بأخذ path ونعود ب onFound مع path

    public static void checkIfFileExists(String md5, final OnFinish onFinish) {
        if (md5 == null && onFinish != null) onFinish.onNotFound();

        FirebaseDatabase.getInstance().getReference().child("hash").child(md5).addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                //file not exists
                if (dataSnapshot.getValue() == null) {
                    if (onFinish != null) {
                        onFinish.onNotFound();
                    }
                } else {
                    //file exists
                    if (onFinish != null) {
                        //file path on Firebase Storage
                        String path = dataSnapshot.getValue(String.class);
                        onFinish.onFound(path);
                    }
                }

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });
    }

 

نعود الآن الى MainActivity الى onActivityResult ونقوم بالتحقق قبل الرفع فيما اذا كان الmd5 موجود مسبقاً ام لا

اذا كان موجود ,فسنكتفي الآن بإظهار Toast مع مسار الملف, وإلا سنقوم برفع الملف بشكل طبيعي

        if (resultCode == RESULT_OK) {
            final Uri imageUri = data.getData();
            Log.d("3llomi", "IMAGE URI is " + imageUri);
            File imageFile = new File(getPath(imageUri));
            Log.d("3llomi", "image file path is " + imageFile.getPath());
            final String md5 = MD5Util.calculateMD5(imageFile);



            FireUtil.checkIfFileExists(md5, new FireUtil.OnFinish() {
                @Override
                public void onFound(String path) {
                    Toast.makeText(MainActivity.this, "image is already uploaded, here is the path.. "+path, Toast.LENGTH_SHORT).show();
                }

                @Override
                public void onNotFound() {
                    //file not found ,upload it
                    upload(imageUri, md5);

                }
            });


        } else {
            Toast.makeText(this, "no image selected :/", Toast.LENGTH_SHORT).show();
        }
    }

نعود مرة أخرى الى التطبيق ونجرب رفع نفس الصورة

فسنجد هذا التوست الجميل الذي يقول أن هذه الصورة فعلا موجودة وهذا هو مسارها

 

المشروع كاملاً على Github 

ملاحظة:المشروع على Github للمعاينة فقط ولايمكنك تجربته على Android Studio لعدم وجود google-services.json الخاص بك  

كلمات دليلية: firebase hashing md5 android
6
إعجاب
2295
مشاهدات
0
مشاركة
0
متابع
متميز
محتوى رهيب

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !