منع رفع الملفات المكررة على Firebase Storage
كيفية منع رفع الملفات المكررة على Firebase Storage عبر MD5 Hashing
سنشرح في هذا الدرس عن كيفية منع رفع الملفات المكررة الى 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 الخاص بك
التعليقات (0)
لايوجد لديك حساب في عالم البرمجة؟
تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !