求助:基于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
相关产品推荐
相关产品推荐

