#1249 fixed copying content URI items

This commit is contained in:
Thibault Deckers 2024-10-29 01:06:46 +01:00
parent ccbca7c506
commit c6ec5afba1
3 changed files with 80 additions and 66 deletions

View file

@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- crash when loading large collection - crash when loading large collection
- Viewer: copying content URI item
## <a id="v1.11.16"></a>[v1.11.16] - 2024-10-10 ## <a id="v1.11.16"></a>[v1.11.16] - 2024-10-10

View file

@ -137,8 +137,7 @@ abstract class ImageProvider {
"success" to false, "success" to false,
) )
// prevent naming with a `.` prefix as it would hide the file and remove it from the Media Store if (sourcePath != null) {
if (sourcePath != null && !desiredName.startsWith('.')) {
try { try {
var newFields: FieldMap = skippedFieldMap var newFields: FieldMap = skippedFieldMap
if (!isCancelledOp()) { if (!isCancelledOp()) {
@ -570,6 +569,20 @@ abstract class ImageProvider {
} }
} }
fun createTimeStampFileName() = Date().time.toString()
private fun sanitizeDesiredFileName(desiredName: String): String {
var name = desiredName
// prevent creating hidden files
while (name.isNotEmpty() && name.startsWith(".")) {
name = name.substring(1)
}
if (name.isEmpty()) {
name = createTimeStampFileName()
}
return name
}
// returns available name to use, or `null` to skip it // returns available name to use, or `null` to skip it
suspend fun resolveTargetFileNameWithoutExtension( suspend fun resolveTargetFileNameWithoutExtension(
contextWrapper: ContextWrapper, contextWrapper: ContextWrapper,
@ -578,18 +591,19 @@ abstract class ImageProvider {
mimeType: String, mimeType: String,
conflictStrategy: NameConflictStrategy, conflictStrategy: NameConflictStrategy,
): NameConflictResolution { ): NameConflictResolution {
var resolvedName: String? = desiredNameWithoutExtension val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
var resolvedName: String? = sanitizedNameWithoutExtension
var replacementFile: File? = null var replacementFile: File? = null
val extension = extensionFor(mimeType) val extension = extensionFor(mimeType)
val targetFile = File(dir, "$desiredNameWithoutExtension$extension") val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
when (conflictStrategy) { when (conflictStrategy) {
NameConflictStrategy.RENAME -> { NameConflictStrategy.RENAME -> {
var nameWithoutExtension = desiredNameWithoutExtension var nameWithoutExtension = sanitizedNameWithoutExtension
var i = 0 var i = 0
while (File(dir, "$nameWithoutExtension$extension").exists()) { while (File(dir, "$nameWithoutExtension$extension").exists()) {
i++ i++
nameWithoutExtension = "$desiredNameWithoutExtension ($i)" nameWithoutExtension = "$sanitizedNameWithoutExtension ($i)"
} }
resolvedName = nameWithoutExtension resolvedName = nameWithoutExtension
} }

View file

@ -40,6 +40,7 @@ import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.io.SyncFailedException import java.io.SyncFailedException
import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
@ -478,7 +479,6 @@ class MediaStoreImageProvider : ImageProvider() {
"success" to false, "success" to false,
) )
if (sourcePath != null) {
// on API 30 we cannot get access granted directly to a volume root from its document tree, // on API 30 we cannot get access granted directly to a volume root from its document tree,
// but it is still less constraining to use tree document files than to rely on the Media Store // but it is still less constraining to use tree document files than to rely on the Media Store
// //
@ -496,7 +496,7 @@ class MediaStoreImageProvider : ImageProvider() {
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
try { try {
val appDir = when { val appDir = when {
toBin -> StorageUtils.trashDirFor(activity, sourcePath) toBin -> StorageUtils.trashDirFor(activity, sourcePath ?: StorageUtils.getPrimaryVolumePath(activity))
toVault -> File(targetDir) toVault -> File(targetDir)
else -> null else -> null
} }
@ -511,8 +511,8 @@ class MediaStoreImageProvider : ImageProvider() {
if (effectiveTargetDir != null) { if (effectiveTargetDir != null) {
val newFields = if (isCancelledOp()) skippedFieldMap else { val newFields = if (isCancelledOp()) skippedFieldMap else {
val sourceFile = File(sourcePath) val sourceFile = if (sourcePath != null) File(sourcePath) else null
if (!sourceFile.exists() && toBin) { if (sourceFile != null && !sourceFile.exists() && toBin) {
delete(activity, sourceUri, sourcePath, mimeType = mimeType) delete(activity, sourceUri, sourcePath, mimeType = mimeType)
deletedFieldMap deletedFieldMap
} else { } else {
@ -522,7 +522,7 @@ class MediaStoreImageProvider : ImageProvider() {
sourceUri = sourceUri, sourceUri = sourceUri,
targetDir = effectiveTargetDir, targetDir = effectiveTargetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
desiredName = desiredName ?: sourceFile.name, desiredName = desiredName ?: sourceFile?.name ?: sourceUri.lastPathSegment ?: createTimeStampFileName(),
nameConflictStrategy = nameConflictStrategy, nameConflictStrategy = nameConflictStrategy,
mimeType = mimeType, mimeType = mimeType,
copy = copy, copy = copy,
@ -536,7 +536,6 @@ class MediaStoreImageProvider : ImageProvider() {
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e) Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e)
} }
}
callback.onSuccess(result) callback.onSuccess(result)
} }
} }
@ -544,7 +543,7 @@ class MediaStoreImageProvider : ImageProvider() {
private suspend fun moveSingle( private suspend fun moveSingle(
activity: Activity, activity: Activity,
sourceFile: File, sourceFile: File?,
sourceUri: Uri, sourceUri: Uri,
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
@ -554,8 +553,8 @@ class MediaStoreImageProvider : ImageProvider() {
copy: Boolean, copy: Boolean,
toBin: Boolean, toBin: Boolean,
): FieldMap { ): FieldMap {
val sourcePath = sourceFile.path val sourcePath = sourceFile?.path
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) } val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) { if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
// nothing to do unless it's a renamed copy // nothing to do unless it's a renamed copy
return skippedFieldMap return skippedFieldMap