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
    }
}
반응형