Fix folder structure and improve no-remote-hash verification
Copier: preserveFolderStructure now uses photo.sourceFolder directly (always correct) instead of parsing devicePath, and applies regardless of how many source folders are in the job. Verification without remote hash: compare file size against ADB-reported size (from ls -la) in addition to checking non-empty. Record local SHA-256 so there's always a checksum on file even without a remote one. VerifyScreen: show 'local SHA-256: abc123...' for copied-without-remote- hash files instead of '(no remote checksum)'.
This commit is contained in:
@@ -63,14 +63,15 @@ class Copier(
|
|||||||
|
|
||||||
photosToImport.forEachIndexed { idx, photo ->
|
photosToImport.forEachIndexed { idx, photo ->
|
||||||
onProgress(idx, photosToImport.size, photo.filename)
|
onProgress(idx, photosToImport.size, photo.filename)
|
||||||
// Determine destination directory — flat or preserve subfolder
|
// Determine destination directory
|
||||||
val destDir = if (job.preserveFolderStructure) {
|
val destDir = if (job.preserveFolderStructure) {
|
||||||
// devicePath is like /sdcard/DCIM/Camera/IMG_001.jpg
|
// Always use sourceFolder regardless of how many folders are in the job
|
||||||
// Extract the subfolder name relative to DCIM root
|
val subFolder = photo.sourceFolder.ifBlank {
|
||||||
val subFolder = photo.devicePath
|
photo.devicePath
|
||||||
.substringAfter("/DCIM/", "")
|
.substringAfter("/DCIM/", "")
|
||||||
.substringBeforeLast("/", "")
|
.substringBeforeLast("/", "")
|
||||||
.ifBlank { "Camera" }
|
.ifBlank { "Camera" }
|
||||||
|
}
|
||||||
stagingDir.resolve(subFolder).also { it.toFile().mkdirs() }
|
stagingDir.resolve(subFolder).also { it.toFile().mkdirs() }
|
||||||
} else {
|
} else {
|
||||||
stagingDir
|
stagingDir
|
||||||
@@ -113,26 +114,34 @@ class Copier(
|
|||||||
pulledFile to false
|
pulledFile to false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Hash the destination file
|
// Step 3: Hash the destination file (always — gives us a local checksum on record)
|
||||||
val destHash = sha256(finalFile.inputStream())
|
val destHash = sha256(finalFile.inputStream())
|
||||||
|
val destSize = finalFile.toFile().length()
|
||||||
|
|
||||||
// Step 4: Get remote hash for cross-check (Android only; iOS returns null)
|
// Step 4: Get remote hash for cross-check (Android only; iOS returns null)
|
||||||
val remoteHash = connector.remoteChecksum(device, photo)
|
val remoteHash = connector.remoteChecksum(device, photo)
|
||||||
|
|
||||||
// Verification: if we have a remote hash, compare; otherwise mark verified
|
val verified: Boolean
|
||||||
// (the copy itself is the verification for iOS since we have no remote hash)
|
val verificationNote: String?
|
||||||
val verified = when {
|
|
||||||
|
when {
|
||||||
wasConverted -> {
|
wasConverted -> {
|
||||||
// HEIC was converted — we can't compare against original hash.
|
// HEIC converted — can't compare hashes, check file is non-empty
|
||||||
// We accept the file if it exists and is non-zero.
|
verified = destSize > 0
|
||||||
finalFile.exists() && finalFile.toFile().length() > 0
|
verificationNote = if (verified) "Converted HEIC→JPEG" else null
|
||||||
}
|
}
|
||||||
remoteHash != null -> {
|
remoteHash != null -> {
|
||||||
remoteHash.equals(destHash, ignoreCase = true)
|
// Full hash comparison
|
||||||
|
verified = remoteHash.equals(destHash, ignoreCase = true)
|
||||||
|
verificationNote = null
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// No remote hash — verify that file is non-empty and readable
|
// No remote hash — verify size matches what ADB reported
|
||||||
finalFile.exists() && finalFile.toFile().length() > 0
|
// (sizeBytes from ls -la, so we know what the device said)
|
||||||
|
val sizeMatch = photo.sizeBytes == 0L || // unknown size is OK
|
||||||
|
destSize == photo.sizeBytes
|
||||||
|
verified = destSize > 0 && sizeMatch
|
||||||
|
verificationNote = if (verified) "Size verified (${destSize / 1024} KB)" else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +154,7 @@ class Copier(
|
|||||||
convertedFromHeic = wasConverted,
|
convertedFromHeic = wasConverted,
|
||||||
error = when {
|
error = when {
|
||||||
!verified && remoteHash != null -> "Hash mismatch — file may be corrupt"
|
!verified && remoteHash != null -> "Hash mismatch — file may be corrupt"
|
||||||
!verified -> "Copy failed — file missing or empty"
|
!verified -> "Copy failed — file missing or size mismatch"
|
||||||
else -> null
|
else -> null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ private fun CopyResultRow(result: CopyResult) {
|
|||||||
when {
|
when {
|
||||||
isFailed -> result.error ?: "Failed"
|
isFailed -> result.error ?: "Failed"
|
||||||
isVerifiedWithHash -> "SHA-256 verified · ${result.photo.sizeBytes / 1024} KB"
|
isVerifiedWithHash -> "SHA-256 verified · ${result.photo.sizeBytes / 1024} KB"
|
||||||
else -> "Copied · ${result.photo.sizeBytes / 1024} KB (no remote checksum)"
|
else -> "Copied · ${result.photo.sizeBytes / 1024} KB · local SHA-256: ${result.destSha256.take(12)}…"
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = when {
|
color = when {
|
||||||
|
|||||||
Reference in New Issue
Block a user