안녕하세요. 오늘은 안드로이드에서 구글 드라이브로 앱 데이터 백업 구현하는법에 대해 포스팅해보겠습니다.
앱 내에서 로컬 DB를 사용하여 데이터를 저장할때, 다른 기기에서도 데이터 동기화를 사용하고싶거나, 데이터를 백업 할 때 사용할 수 있습니다.
아래 포스팅에서는 두 가지의 파일타입 백업/복원 방법을 구현하고있습니다.
- Room Database 파일
- 앱 내부 디렉토리에 저장된 이미지파일(jpeg, png 등)
0. 구글 드라이브로 백업을 하기위해서는, 구글 로그인이 선행 구현 되어있어야합니다.
구글 로그인 관련한 구현은 다른 포스팅을 참고 바랍니다.
1. 프로젝트 수준의 build.gradle에 의존성추가
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.3.15'
}
}
plugins {
id 'com.google.gms.google-services' version '4.3.15' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
2. 앱 수준의 build.gradle에 의존성 추가
dependencies {
implementation 'com.google.api-client:google-api-client-android:1.23.0'
implementation 'com.google.apis:google-api-services-drive:v3-rev136-1.25.0'
implementation 'com.google.auth:google-auth-library-oauth2-http:0.1.0'
implementation 'com.google.http-client:google-http-client-gson:1.19.0'
}
3. 백업하기
* 해당 코드는 Activity에 구현되어있습니다. 필요한곳에서 적절히 변형하여 사용하시면 됩니다.
private val googleDriveBackupLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
onBackup()
}
}
private fun onBackup() {
lifecycleScope.launch {
var httpRequestInitializer: HttpRequestInitializer? = null
try {
httpRequestInitializer = HttpRequestInitializer { httpRequest ->
val credentials = GoogleAccountCredential.usingOAuth2(
this@MainActivity,
listOf(DriveScopes.DRIVE_APPDATA),
).apply {
selectedAccountName = googleSignInAccount?.account?.name // 구글 로그인 정보 가져오기
}
httpRequest.connectTimeout = 3 * 60000 // 타임아웃 설정
httpRequest.readTimeout = 3 * 60000 // 타임아웃 설정
credentials?.initialize(httpRequest)
}
} catch (e: Exception) {
Log.e(TAG, "backup failed : ${e.message}")
}
val service: Drive = Drive.Builder(
NetHttpTransport(),
GsonFactory.getDefaultInstance(),
httpRequestInitializer,
).setApplicationName(getApplicationName()).build()
try {
val dbPath = baseContext.getDatabasePath(DB_NAME).absolutePath // room db path 가져오기
val dbShmPath = "$dbPath-shm"
val dbWalPath = "$dbPath-wal"
val storageDbFile = com.google.api.services.drive.model.File().apply {
this.parents = listOf("appDataFolder")
this.name = DB_FILE_NAME // App db name 사용
}
val storageDbShmFile = com.google.api.services.drive.model.File().apply {
this.parents = listOf("appDataFolder")
this.name = DB_SHM_FILE_NAME // room db 사용시에만 해당
}
val storageDbWalFile = com.google.api.services.drive.model.File().apply {
this.parents = listOf("appDataFolder")
this.name = DB_WAL_FILE_NAME // room db 사용시에만 해당
}
val dbFilePath = File(dbPath)
val dbShmFilePath = File(dbShmPath)
val dbWalFilePath = File(dbWalPath)
val dbMediaContent = FileContent("application/octet-stream", dbFilePath)
val dbShmMediaContent = FileContent("application/octet-stream", dbShmFilePath)
val dbWalMediaContent = FileContent("application/octet-stream", dbWalFilePath)
val uploadDbFileDefList = listOf(
async(Dispatchers.IO) {
service.files().create(storageDbFile, dbMediaContent).execute()
},
async(Dispatchers.IO) {
service.files().create(storageDbShmFile, dbShmMediaContent).execute()
},
async(Dispatchers.IO) {
service.files().create(storageDbWalFile, dbWalMediaContent).execute()
},
)
uploadDbFileDefList.awaitAll() // db 파일 업로드 대기
val imageFileContentList: List<File> =
File(baseContext.getDir("imageDir", Context.MODE_PRIVATE).absolutePath).listFiles()
?.toList() ?: throw IllegalStateException("image file found error")
val imageMediaContentPair = imageFileContentList.map {
Pair(
com.google.api.services.drive.model.File().apply {
this.parents = listOf("appDataFolder")
this.name = it.name
},
FileContent("image/jpeg", it),
)
}
// 이미지 업로드에는 시간이 오래걸리므로 async를 사용하여 병렬 처리를 합시다.
val imageDefList = mutableListOf<Deferred<com.google.api.services.drive.model.File>>()
imageMediaContentPair.forEachIndexed { index, pair ->
val def = async(Dispatchers.IO) {
service.files().create(pair.first, pair.second).execute()
}
imageDefList.add(def)
}
imageDefList.awaitAll()
} catch (e: GoogleJsonResponseException) {
Log.e(TAG, "backup failed : ${e.message}")
} catch (e: UserRecoverableAuthIOException) {
googleDriveBackupLauncher.launch(e.intent) // 해당 exception은 잡아서 따로 처리를 해줘야합니다.
} catch (e: Exception) {
Log.e(TAG, "backup failed : ${e.message}")
}
}
}
3. 복원하기
private val googleDriveRestoreLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
onRestore()
}
}
private fun onRestore() {
lifecycleScope.launch {
var httpRequestInitializer: HttpRequestInitializer? = null
try {
httpRequestInitializer = HttpRequestInitializer { httpRequest ->
val credentials = GoogleAccountCredential.usingOAuth2(
this@MainActivity,
listOf(DriveScopes.DRIVE_APPDATA),
).apply {
selectedAccountName = googleSignInAccount?.account?.name
}
httpRequest.connectTimeout = 3 * 60000
httpRequest.readTimeout = 3 * 60000
credentials?.initialize(httpRequest)
}
} catch (e: Exception) {
//
}
val service: Drive = Drive.Builder(
NetHttpTransport(),
GsonFactory.getDefaultInstance(),
httpRequestInitializer,
).setApplicationName(getApplicationName()).build()
try {
val fileList = mutableListOf<com.google.api.services.drive.model.File>()
// 처음 20개 가져오기
val firstFetch = withContext(Dispatchers.IO) {
service.files()
.list()
.setSpaces("appDataFolder")
.setFields("nextPageToken, files(id, name, createdTime, mimeType)")
.setPageSize(20)
.execute()
}
if (firstFetch.files.size == 0) { // empty drive
//
} else {
firstFetch.files.forEach { fileList.add(it) }
}
var pageToken = firstFetch.nextPageToken
// 20개씩 페이징해서 전체 목록 가져오기
while (pageToken != null) {
val fetch = withContext(Dispatchers.IO) {
service.files()
.list()
.setSpaces("appDataFolder")
.setPageToken(pageToken)
.setFields("nextPageToken, files(id, name, createdTime, mimeType)")
.setPageSize(20)
.execute()
}
if (fetch.files.size == 0) {
//
} else {
fetch.files.forEach { fileList.add(it) }
pageToken = fetch.nextPageToken
}
}
val dbPath = getDatabasePath(DB_NAME).absolutePath
// save restore Data
val defList = mutableListOf<Deferred<Unit>>()
// 가져온 파일 내부 디렉토리에 저장
fileList.forEach { file ->
when (file.mimeType) {
"application/octet-stream" -> {
val outputStreamPath = when (file.name) {
DB_FILE_NAME -> dbPath
DB_SHM_FILE_NAME -> "$dbPath-shm"
DB_WAL_FILE_NAME -> "$dbPath-wal"
else -> ""
}
if (outputStreamPath.isNotEmpty()) {
val outputStream = FileOutputStream(outputStreamPath)
val def = async(Dispatchers.IO) {
service.files().get(file.id).executeMediaAndDownloadTo(outputStream)
}
defList.add(def)
}
}
IMAGE_MIME_TYPE -> {
val def = async(Dispatchers.IO) {
val imagePath = File(
baseContext.getDir("imageDir", Context.MODE_PRIVATE),
file.name
)
val outputStream = FileOutputStream(imagePath)
service.files().get(file.id).executeMediaAndDownloadTo(outputStream)
println("Download Image : ${file.name} / ${file.id} / ${file.mimeType}")
}
defList.add(def)
}
}
}
defList.awaitAll()
// 앱을 재시작해줘야 복원된 데이터가 반영됩니다.
val pm = packageManager
val intent = pm.getLaunchIntentForPackage(packageName)
val componentName = intent?.component
val mainIntent = Intent.makeRestartActivityTask(componentName)
startActivity(mainIntent)
Runtime.getRuntime().exit(0)
} catch (e: GoogleJsonResponseException) {
Log.e(TAG, e.message.toString())
} catch (e: UserRecoverableAuthIOException) {
googleDriveRestoreLauncher.launch(e.intent)
} catch (e: Exception) {
Log.e(TAG, e.message.toString())
}
}
}
자세한 내용은 구글드라이브V3 공식 문서를 확인하세요.
ref : https://developers.google.com/drive/api/guides/about-sdk?hl=ko