Develop/Flutter
USB 드라이브에 파일 저장
AQoong
2025. 4. 18. 14:38
Flutter 로 Android 전용 앱을 만드는 중 확인한 현상을 기록합니다.
Android산업용 보드에 앱을 만들어 서비스해야하는 상황으로 USB드라이브를 연결하여 사용자가 선택하는 위치에 파일을 생성해야합니다.
- USB가 마운트되면 위치가 별도의 ID(xxxx-xxxx)가 사용되므로 Path를 개발자가 예상할 수 없음
- Android 10 이상에서는 Scoped Storage가 적용되어 SAF(Storage Access Framework)를 사용해야만 함
- Flutter 패키지들이 있지만 몇가지 버그 및 유지보수가 안되고 있음
직접 Kotlin으로 기능 구현
SAF 파일 앱 연동
//파일앱 열기
fun openExplorer(pendingResult: MethodChannel.Result?, fileName: String, fileBytes: ByteArray) {
this.pendingResult = pendingResult
this.fileName = fileName
this.fileBytes = fileBytes
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
activity.startActivityForResult(intent, activity.FILE_EXPLORER_RESULT)
}
//파일앱에서 선택한 Path 결과 (Uri)
fun getExplorerResult(data: Intent?) {
data?.data?.let { uri ->
activity.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
try {
//파일 생성
val result = saveData(uri)
pendingResult?.success(result.toString())
} catch (e: Exception) {
pendingResult?.error("FILE_CREATE_ERROR", e.message, null)
}
} ?: run {
pendingResult?.error("NO_SELECTION", "No directory selected", null)
}
}
파일 생성
fun saveData(treeUri: Uri): Uri {
val documentId = DocumentsContract.getTreeDocumentId(treeUri)
val parentDocumentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
val newFileUri = DocumentsContract.createDocument(
contentResolver,
parentDocumentUri,
"application/octet-stream",
fileName
)
if (newFileUri != null) {
contentResolver.openOutputStream(newFileUri)?.use { stream ->
stream.write(fileBytes)
stream.flush()
result.success(true)
}
} else {
result.error("CREATE_FAILED", "Failed to create file", null)
}
}
문제발생
이렇게 작업을하면 파일생성은 잘 되지만 USB를 안드로이드에서 마운트해제하면 생성됐던 파일의 사이즈가 0바이트로 줄어들면서 파일이 손상됩니다.
이것은 마운트된 디스크와 최후의 sync가 되지않아 발생하는 문제.
해결하기 위한 방법은 flush()는 Java레벨의 버퍼만 비우는데 커널/디스크 레벨까지 모든 캐시를 sync하기위해 fsync()를 호출해야합니다.
수정한 파일 생성
import android.system.Os
...
fun saveData(treeUri: Uri): Uri {
try {
val docId = DocumentsContract.getTreeDocumentId(treeUri)
val parentDocumentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, docId)
val docUri = DocumentsContract.createDocument(
activity.contentResolver,
parentDocumentUri,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
fileName!!
)
if (docUri != null) {
val pfd: ParcelFileDescriptor =
activity.contentResolver.openFileDescriptor(docUri, "w")
?: throw Exception("[FD_FAILED] Could not open ParcelFileDescriptor", null)
try {
FileOutputStream(pfd.fileDescriptor).use { stream ->
stream.write(fileBytes)
stream.flush()
//강제로 디스크에 기록
Os.fsync(pfd.fileDescriptor)
}
} finally {
pfd.close()
}
return docUri
} else {
throw Exception("[CREATE_FAILED] Failed to create file : ${docUri}")
}
} catch (e: Exception) {
throw e
}
}
반응형