You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:基于Google Drive API实现Android应用云备份功能

架构设计指导

1. 核心映射关系维护

由于Google Drive依赖ID而非路径,必须在本地维护本地路径 ↔ Drive文件/文件夹ID的映射表(推荐用Room数据库存储),实现本地路径与云端资源的双向关联。映射表建议包含字段:

  • 本地路径(唯一键)
  • Drive资源ID
  • 资源类型(文件/文件夹)
  • 同步状态(待上传/已同步/待删除)

2. 开关触发逻辑

与iOS iCloud开关逻辑对齐,在设置页实现Google Drive备份开关:

  • 开启时:遍历本地所有未同步资源,递归创建对应Drive目录结构,上传文件并更新映射表
  • 关闭时:可选择保留云端备份或删除,将云端文件迁移回本地原路径,清空映射表(或标记为不同步状态)

3. 递归目录同步处理

Drive创建文件夹需确保父目录存在,因此要递归处理层级结构:

  • 例:本地路径/docs/work/report.pdf,先检查Drive中是否存在docs文件夹,不存在则创建;再检查docs/work,不存在则创建;最后将report.pdf上传至docs/work目录下

4. 冲突处理(可选)

若云端已有同名文件,可采用三种策略:覆盖、本地文件重命名、提示用户选择,建议默认覆盖(与iCloud逻辑一致)


关键代码示例

1. Drive服务初始化

基于Google登录完成Drive v3 API初始化:

class DriveServiceHelper(private val context: Context, private val googleAccount: Account) {
    private val driveResourceClient: DriveResourceClient

    init {
        val signInAccount = GoogleSignIn.getLastSignedInAccount(context)
        val driveOptions = DriveOptions.Builder()
            .setScope(DriveScopes.DRIVE_FILE) // 仅请求文件权限,避免过度授权
            .build()
        driveResourceClient = Drive.getDriveResourceClient(context, signInAccount, driveOptions)
    }
}

2. 递归创建Drive文件夹

根据本地目录层级,递归创建Drive文件夹并返回ID:

suspend fun createFolderRecursive(parentFolderId: String?, folderName: String): String {
    // 检查父目录下是否已存在同名文件夹
    val query = Query.Builder()
        .addFilter(Filters.eq(SearchableField.TITLE, folderName))
        .addFilter(Filters.eq(SearchableField.PARENTS, parentFolderId ?: "root"))
        .build()

    val results = driveResourceClient.query(query).await()
    if (results.count > 0) {
        return results.first().id
    }

    // 创建新文件夹元数据
    val folderMetadata = MetadataChangeSet.Builder()
        .setTitle(folderName)
        .setMimeType(DriveFolder.MIME_TYPE)
        .setParents(listOf(parentFolderId ?: "root"))
        .build()

    val folder = driveResourceClient.createFolder(folderMetadata).await()
    return folder.id
}

3. 上传本地文件到指定Drive文件夹

suspend fun uploadFile(localFilePath: String, driveFolderId: String): String {
    val file = File(localFilePath)
    val fileName = file.name
    val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension) ?: "*/*"

    // 删除云端同名文件(避免冲突)
    val existingFileQuery = Query.Builder()
        .addFilter(Filters.eq(SearchableField.TITLE, fileName))
        .addFilter(Filters.eq(SearchableField.PARENTS, driveFolderId))
        .build()
    val existingFiles = driveResourceClient.query(existingFileQuery).await()
    existingFiles.forEach { driveResourceClient.delete(it).await() }

    // 创建文件元数据
    val metadataChangeSet = MetadataChangeSet.Builder()
        .setTitle(fileName)
        .setMimeType(mimeType)
        .setParents(listOf(driveFolderId))
        .build()

    // 上传文件内容
    val contentUri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
    val driveFile = driveResourceClient.createFile(metadataChangeSet, contentUri).await()
    return driveFile.id
}

4. 开关触发的全量同步逻辑

// SyncDao为Room数据库的映射表数据访问对象
suspend fun syncLocalFilesToDrive(localRootPath: String, syncDao: SyncDao) {
    val localFiles = File(localRootPath).walkTopDown().filter { it.exists() }
    var currentDriveParentId: String? = "root"

    for (file in localFiles) {
        if (file.isDirectory) {
            // 递归创建Drive文件夹层级
            val relativePath = file.relativeTo(File(localRootPath)).path
            val folderNames = relativePath.split(File.separator)
            currentDriveParentId = "root"
            folderNames.forEach { folderName ->
                currentDriveParentId = createFolderRecursive(currentDriveParentId, folderName)
            }
            // 保存映射关系
            syncDao.insertSyncRecord(SyncRecord(file.path, currentDriveParentId, TYPE_FOLDER, STATUS_SYNCED))
        } else {
            // 上传文件到对应Drive文件夹
            val parentFolderPath = file.parent ?: continue
            val parentSyncRecord = syncDao.getSyncRecordByLocalPath(parentFolderPath) ?: continue
            val driveFileId = uploadFile(file.path, parentSyncRecord.driveId)
            // 保存映射关系
            syncDao.insertSyncRecord(SyncRecord(file.path, driveFileId, TYPE_FILE, STATUS_SYNCED))
        }
    }
}

5. 关闭备份时的本地迁移

suspend fun migrateBackToLocal(syncDao: SyncDao, newLocalRootPath: String) {
    val syncRecords = syncDao.getAllSyncRecords()
    syncRecords.forEach { record ->
        if (record.type == TYPE_FILE) {
            // 从Drive下载文件到本地新路径
            val driveFile = driveResourceClient.getFile(record.driveId).await()
            val outputFile = File(newLocalRootPath, File(record.localPath).name)
            FileOutputStream(outputFile).use { outputStream ->
                driveResourceClient.openFile(driveFile, DriveFile.MODE_READ_ONLY).await().use { inputStream ->
                    inputStream.copyTo(outputStream)
                }
            }
        }
        // 可选:删除云端资源,若需保留备份可注释此行
        driveResourceClient.delete(driveResourceClient.getFile(record.driveId).await()).await()
    }
    // 清空映射表
    syncDao.deleteAllSyncRecords()
}

内容的提问来源于stack exchange,提问作者Luis Javier Canto Hurtado

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 06:12:49