سلام.
یکی از روش های کارآمد برای پشتیبان گیری از دیتابیس محلی آپلود کردن یک فایل CSV روی سروره این روش مزایا و معایب خاص خودشو داره که بررسی میکنیم بعد میریم سراغ پیاده سازی .
با توجه به مزایا و معایبی که گفتم بستگی به نوع پروژه شما و تصمیم خودتون داره که از این روش استفاده کنین یا نه . به هر حال بریم سراغ پیاده سازی .
پیش فرض ما تو این سولوشن استفاده از room و retrofit هست.
قبل از همه چیز این خط رو به فایل build.gradle پروژه اضافه کنید و پروژه رو sync کنید
implementation ("com.opencsv:opencsv:5.5.2")
برای اینکه قابل فهم تر باشه از بیرونی ترین فانکشن شروع کنیم و پیش بریم.
1- یه فانکشن بکاپ میسازیم مثل کد زیر:
fun backupDb() { try { val dataBasePart = getDatabaseAsPart() val result = retrofitService.uploadDatabase( dataBasePart ) if (result.isSuccessful) { //پیاده سازی لازم } else { //پیاده سازی لازم } } catch (e: Exception) { e.printStackTrace() } } }
تو این فانکشن دوتا فانکشن دیگه فراخوانی شده یکی کال رتروفیت سرویسمونه یکی دیگه هم فانکشن
getDatabaseAsPart . اول چند تا نکته رو بگم بعد بریم سراغ این دوتا فانکشن .
نکته اول: retrofitService رو باید تو کلاسی که این فانکشن هست اینیشیالایز کنید یا اینجکت کنید که اینجا من نیاوردم.
نکته دوم: باید فایلی که قراره آپلودش کنیم در قالب یک Part به رتروفیت کال داده بشه که فانکشن getDatabaseAsPart این کارو انجام خواهد داد.
نکته سوم: بهتره و یا لازمه که تو آپلود از متدای امنیتی و Authentication و همچینین rate Limiter استفاده کنید که باز برای اینکه قیمه ها رو نریزیم تو ماستا من اینجا نیاوردم.
2- فانکشن رتروفیت سرویس:
@Multipart @POST("backup/uploadDb") suspend fun uploadDatabase( @Part dataBase: MultipartBody.Part ): Response<String>
3- فانکشن getDatabaseAsPart:
fun getDatabaseAsPart( ): MultipartBody.Part { val dbFile: File = csvExportUtil.exportDatabaseToCsv() //این کلاس در ادامه ساخته خواهد شد val requestFile = RequestBody.create("application/octet-stream".toMediaTypeOrNull(), dbFile) return MultipartBody.Part.createFormData("DatabaseFile", dbFile.name, requestFile) }
خب بزارید این فانکشنو یه ذره دقیق تر بررسی کنیم . تو مرحله اول یه مقدار داریم از نوع File که فانکشن exportDatabaseToCsv اون رو به ما حواهد داد که یه فایل csvهست. بعد یه RequestBody از فایلمون میسازیم توسط فانکشن create که دو تا ورودی میگیره و مرحله آخر پارتی که میخواستیمو بر میگردونه.باز یادتون باشه کلاس csvExportUtil رو که خواهیم ساخت در کلاس این فانکشن اینیشیالایز یا اینجکت کنید
بیشتر بدانید!!:
"application/octet-stream"" این MIME type نشاندهنده این است که دادهها به عنوان یک جریان باینری (Binary Stream) هستند و نوع خاصی از دادهها مشخص نشده است. ()toMediaTypeOrNull این تابع رشته را به یک شیء MediaType
تبدیل میکند. اگر تبدیل موفق نباشد، null
برمیگرداند.
خب بریم سراغ فانکشن exportDatabaseToCsv
3- کلاس csvExportUtil و فانکشن exportDatabaseToCsv:
class CsvExportUtil() { fun exportDatabaseToCsv(context:Context):File { val csvFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "name.csv") var csvWriter: CSVWriter? = null try { csvWriter = CSVWriter(FileWriter(csvFile)) var cursor: Cursor? =yourDao.getAllUsersCursor() // فرض کنید این متد تمام ردیفهای جدول را باز میگرداند // نوشتن سطر عنوانها csvWriter.writeNext(cursor!!.columnNames) // نوشتن دادههای جدول while (cursor.moveToNext()) { val row = Array(cursor.columnCount) { i -> cursor.getString(i) } csvWriter.writeNext(row) } cursor.close() } catch (e: Exception) { e.printStackTrace() } finally { try { csvWriter?.close() } catch (e: IOException) { e.printStackTrace() } } return csvFile } }
خب توضیحات این فانکشن اینطوریه که:
یه context میگیره که برا کلاس File لازمش داریم .
csvFile یه فایل تو پوشه داکیومنت میسازه با پسوند csv و اسم name .
یه مقدار میسازیم از نوع CSVWriter که فایل رو تو تابع FileWriter از ما به عنوان ورودی می گیره.
بعد یه cursor میسازیم که اطلاعاتو از جدول مورد نظرمون میخونه که فانکشنشو تو ادامه مینویسم براتون .
حالا به تابع csvWriter دستور میدیم که اسم ستون های جدولمون رو ت سطر اول بنویسه
بعدش هم با یه loop خط به خط اطلاعات جدولمون رو تو فایل csv می نویسیم . ( برا اینکه مسئله روشنتر شه یه نمونه فایل csv رو ببینید) .
در آخر هم cursor و writer رو بستیم که دچار memory leak نشیم.
بیشتر بدانید:
4- فانکشن getAllUsersCursor :
@Query("SELECT * FROM users") fun getAllUsersCursor(): Cursor
تو این فانکشن همه یوزر هامونو در قالب یک Cursor از تیبل میکشیم بیرون و تقریبا کارمون با اندروید تمومه.
خب از اونجاییکه من android developer هستم فقط فانکشن های سمت بک اند رو بدون توضیح کلی در اختیارتون میزارم و اگر اشتباهی تو این موارد هست از شما و متخصصینش عذر خواهی میکنم.
1-کنترلر backup endpoint
public static class BackUpEndPoints { public static RouteGroupBuilder MapBackupEndPoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/backup").WithParameterValidation(); group.MapPost("/uploadDb",async ( IFileService iFileService, [FromForm] GetDatabaseDto dbDto )=>{ if (dbDto.DatabaseFile != null) { var fileResult = iFileService.SaveDatabase(dbDto.DatabaseFile,"folder name" ); if (fileResult.Item1 == 1) { return Results.Ok("با موفقیت آپلود شد"); } else { return Results.Conflict(new { error = fileResult.Item2}); } } else { return Results.Conflict(new { error = "فایل خالی است"}); } }); return group; } }
2- اینترفیس IFileService
public interface IFileService { public Tuple<int, string> SaveDatabase(IFormFile sqlightDb,String userphone); //تابع پایینی برای دیافت بکاپ از سرور public Tuple<int, string> GetDatabasePath(String userphone); }
3- پیاده سازی اینترفیس IFileService
public class FileService:IFileService { private IWebHostEnvironment _environment; public FileService(IWebHostEnvironment environment) { _environment = environment; } private readonly string _backupPath = "/app/files/backups" public Tuple<int, string> SaveDatabase(IFormFile sqlightDb, string folderName) { try { var contentPath = this._environment.ContentRootPath; var path = Path.Combine(_backupPath, folderName); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } var ext = Path.GetExtension(sqlightDb.FileName); var allowedExtensions = new string[] { ".csv" }; if (!allowedExtensions.Contains(ext)) { string msg = string.Format("Only {0} extensions are allowed", string.Join(",", allowedExtensions)); return new Tuple<int, string>(0, msg); } var newFileName = "users" var fileWithPath = Path.Combine(path, newFileName); if (System.IO.File.Exists(fileWithPath)) { System.IO.File.Delete(fileWithPath); } using (var stream = new FileStream(fileWithPath, FileMode.Create)) { sqlightDb.CopyTo(stream); } return new Tuple<int, string>(1, "backups/" + folderName+ "/" + newFileName); } catch (Exception ex) { return new Tuple<int, string>(0, "Error has occured"); } } }
در آخر این چند خط رو به program .cs اضافه کنید:
//var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IFileService, FileService>(); //var app = builder.Build(); app.MapBackupEndPoints();
مجددا عرض کنم خدمتتون من متخصص دات نت نیستم و این چند خط رو ارائه کردم برا شما عزیزان اندروید دولوپری که خودتون بک اند رو میزنین . فقط خواستم یه ذهنیتی براتون ایجاد بشه نه اینکه دقیقا این کد رو پیاده سازی کنین.
تو پست بعدی توضیحات دریافت فایل پشتیبان رو منتشر می کنم . امیدوارم که تا اینجاش قابل فهم بوده باشه و اگر اشکالی هست ممنون میشم تو نظرات بهم بگین . فعلا خدا حافظ...