Skip to content

Commit 8a66346

Browse files
committed
Explorer: Implement CreateOperationHandler fully
1 parent 8e70a2a commit 8a66346

2 files changed

Lines changed: 170 additions & 36 deletions

File tree

  • app-common-io/src/main/java/eu/darken/butler/common/files/operations
  • app-workspace-explorer/src/main/java/eu/darken/butler/explorer/core/operations/handlers

app-common-io/src/main/java/eu/darken/butler/common/files/operations/Issue.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ sealed interface Issue {
2020
override val issueId: IssueId = IssueId(),
2121
val source: APathLookup<APath>? = null,
2222
val destination: APathLookup<APath>,
23-
val canSkip: Boolean = true,
24-
val canOverwrite: Boolean = true,
25-
val canRenameSource: Boolean = true,
26-
val canRenameDestination: Boolean = true,
27-
val suggestedName: String? = null,
23+
val canSkip: Boolean = false,
24+
val canOverwrite: Boolean = false,
2825
val canMerge: Boolean = false,
26+
val canRenameSource: Boolean = false,
27+
val canRenameDestination: Boolean = false,
28+
val suggestedName: String? = null,
2929
) : Issue {
3030
sealed interface Resolution : Issue.Resolution {
3131
data class Skip(val applyToAll: Boolean = false) : Resolution
@@ -42,7 +42,7 @@ sealed interface Issue {
4242
override val issueId: IssueId = IssueId(),
4343
val source: APathLookup<APath>,
4444
val destination: APathLookup<APath>,
45-
val canSkip: Boolean = true,
45+
val canSkip: Boolean = false,
4646
) : Issue {
4747
sealed interface Resolution : Issue.Resolution {
4848
data class Skip(val applyToAll: Boolean = false) : Resolution
@@ -54,7 +54,7 @@ sealed interface Issue {
5454
override val issueId: IssueId = IssueId(),
5555
val source: APathLookup<APath>,
5656
val destination: APathLookup<APath>,
57-
val canSkip: Boolean = true,
57+
val canSkip: Boolean = false,
5858
) : Issue {
5959
sealed interface Resolution : Issue.Resolution {
6060
data class Skip(val applyToAll: Boolean = false) : Resolution
@@ -68,7 +68,7 @@ sealed interface Issue {
6868
val destination: APathLookup<APath>? = null,
6969
val exception: Throwable,
7070
val errorMessage: CaString = caString { exception.localized(it).description.get(it) },
71-
val canSkip: Boolean = true,
71+
val canSkip: Boolean = false,
7272
val canRetry: Boolean = false,
7373
) : Issue {
7474
sealed interface Resolution : Issue.Resolution {

app-workspace-explorer/src/main/java/eu/darken/butler/explorer/core/operations/handlers/CreateOperationHandler.kt

Lines changed: 162 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@ package eu.darken.butler.explorer.core.operations.handlers
33
import dagger.assisted.Assisted
44
import dagger.assisted.AssistedFactory
55
import dagger.assisted.AssistedInject
6+
import eu.darken.butler.common.ca.toCaString
67
import eu.darken.butler.common.coroutine.DispatcherProvider
8+
import eu.darken.butler.common.debug.logging.Logging.Priority.*
79
import eu.darken.butler.common.debug.logging.log
810
import eu.darken.butler.common.debug.logging.logTag
11+
import eu.darken.butler.common.files.APath
912
import eu.darken.butler.common.files.GatewaySwitch
10-
import eu.darken.butler.common.files.extensions.copyOperation
1113
import eu.darken.butler.common.files.extensions.deleteWalk
1214
import eu.darken.butler.common.files.extensions.exists
1315
import eu.darken.butler.common.files.extensions.lookup
1416
import eu.darken.butler.common.files.operations.Issue
17+
import eu.darken.butler.common.files.operations.MoveOperation
1518
import eu.darken.butler.explorer.core.engine.ExplorerOperation
1619
import eu.darken.butler.explorer.core.operations.IssueHandler
1720
import eu.darken.butler.explorer.core.operations.OperationContext
1821
import eu.darken.butler.explorer.core.operations.OperationNotifier
1922
import eu.darken.butler.workspace.core.Workspace
2023
import kotlinx.coroutines.CancellationException
24+
import kotlinx.coroutines.currentCoroutineContext
2125
import kotlinx.coroutines.flow.last
26+
import kotlinx.coroutines.isActive
2227

2328
class CreateOperationHandler @AssistedInject constructor(
2429
@Assisted workspaceId: Workspace.Id,
@@ -33,60 +38,189 @@ class CreateOperationHandler @AssistedInject constructor(
3338

3439
private val tag = logTag("Explorer", "Workspace", workspaceId.shortTag, "Operation", "Create")
3540

36-
override suspend fun executeInContext(
41+
private suspend fun resolveNestedConflicts(
3742
context: OperationContext,
38-
operation: ExplorerOperation.FileOp.Create,
39-
): Unit = with(context) {
40-
log(tag) { "executeCreateFolder(): $operation" }
41-
var targetPath = operation.parentPath.child(operation.name)
43+
parentPath: APath,
44+
initialName: String,
45+
parentIssue: Issue.PathAlreadyExists,
46+
): APath = with(context) {
47+
var currentName = initialName
4248

43-
if (targetPath.exists(gatewaySwitch)) {
44-
val issue = Issue.PathAlreadyExists(
45-
destination = targetPath.lookup(gatewaySwitch),
49+
while (currentCoroutineContext().isActive) {
50+
val currentPath = parentPath.child(currentName)
51+
if (!currentPath.exists(gatewaySwitch)) break
52+
53+
val nestedIssue = parentIssue.copy(
54+
destination = currentPath.lookup(gatewaySwitch),
4655
)
56+
log(tag, INFO) { "resolveNestedConflicts(): Nested conflict: $nestedIssue" }
4757

48-
val resolution = issueHandler.handleIssue(
58+
val nestedResolution = issueHandler.handleIssue(
4959
context = context,
50-
issue = issue,
60+
issue = nestedIssue,
5161
) as Issue.PathAlreadyExists.Resolution
62+
log(tag, INFO) { "resolveNestedConflicts(): Resolution: $nestedResolution" }
5263

53-
when (resolution) {
64+
when (nestedResolution) {
5465
is Issue.PathAlreadyExists.Resolution.Skip -> {
55-
return
66+
throw CancellationException("Nested conflict skipped")
5667
}
5768
is Issue.PathAlreadyExists.Resolution.RenameSource -> {
58-
targetPath = operation.parentPath.child(resolution.newName)
69+
currentName = nestedResolution.newName
5970
}
6071
is Issue.PathAlreadyExists.Resolution.RenameDestination -> {
61-
// Rename the existing file/folder to make room for the new one
62-
val existingPath = targetPath
63-
val newExistingPath = operation.parentPath.child(resolution.newName)
64-
existingPath.copyOperation(
65-
gateway = gatewaySwitch,
66-
target = newExistingPath,
67-
overwrite = false
68-
).last()
69-
existingPath.deleteWalk(gatewaySwitch)
72+
throw IllegalArgumentException("Cannot rename destination when renaming existing file")
7073
}
7174
is Issue.PathAlreadyExists.Resolution.Overwrite -> {
72-
// Delete existing file/folder before creating new one
73-
targetPath.deleteWalk(gatewaySwitch)
75+
currentPath.deleteWalk(gatewaySwitch)
76+
break // Exit conflict loop, path is now clear
7477
}
7578
is Issue.PathAlreadyExists.Resolution.Merge -> {
76-
throw IllegalArgumentException("Can't merge on create")
79+
throw IllegalArgumentException("Cannot merge when renaming existing file")
7780
}
7881
is Issue.PathAlreadyExists.Resolution.Cancel -> {
7982
throw CancellationException("Operation cancelled")
8083
}
8184
}
8285
}
8386

84-
gatewaySwitch.createDir(targetPath)
87+
return parentPath.child(currentName)
88+
}
89+
90+
override suspend fun execute(
91+
context: OperationContext,
92+
operation: ExplorerOperation.FileOp.Create,
93+
): Unit = with(context) {
94+
log(tag) { "execute(): $operation" }
95+
96+
var currentOperation = operation
97+
var destinationPath: APath
98+
99+
// Loop to handle conflicts until we have a clear destination path
100+
while (currentCoroutineContext().isActive) {
101+
destinationPath = currentOperation.parentPath.child(currentOperation.name)
102+
103+
if (!destinationPath.exists(gatewaySwitch)) {
104+
break // No conflict, proceed to creation
105+
}
106+
107+
val issue = Issue.PathAlreadyExists(
108+
destination = destinationPath.lookup(gatewaySwitch),
109+
canRenameSource = true,
110+
canRenameDestination = true,
111+
canOverwrite = true,
112+
)
113+
log(tag, INFO) { "execute(): Issue: $issue" }
114+
115+
val resolution = issueHandler.handleIssue(
116+
context = context,
117+
issue = issue,
118+
) as Issue.PathAlreadyExists.Resolution
119+
log(tag, INFO) { "execute(): Issue: $issue - Resolution: $resolution" }
120+
121+
when (resolution) {
122+
is Issue.PathAlreadyExists.Resolution.Skip -> return
123+
124+
is Issue.PathAlreadyExists.Resolution.RenameSource -> {
125+
currentOperation = currentOperation.copy(name = resolution.newName)
126+
// Continue loop to check if new name also conflicts
127+
}
128+
129+
is Issue.PathAlreadyExists.Resolution.RenameDestination -> {
130+
val resolvedPath = resolveNestedConflicts(
131+
context = context,
132+
parentPath = currentOperation.parentPath,
133+
initialName = resolution.newName,
134+
parentIssue = issue,
135+
)
136+
137+
// Now perform the move with the resolved destination
138+
while (currentCoroutineContext().isActive) {
139+
try {
140+
gatewaySwitch.move(destinationPath, resolvedPath, MoveOperation.Options()).last()
141+
break // Move succeeded, exit loop
142+
} catch (e: Exception) {
143+
val moveIssue = Issue.UnknownError(
144+
exception = e,
145+
errorMessage = (e.message ?: e.toString()).toCaString(),
146+
source = destinationPath.lookup(gatewaySwitch),
147+
destination = resolvedPath.lookup(gatewaySwitch),
148+
canRetry = true,
149+
canSkip = false,
150+
)
151+
when (issueHandler.handleIssue(context, moveIssue) as Issue.UnknownError.Resolution) {
152+
is Issue.UnknownError.Resolution.Retry -> continue
153+
is Issue.UnknownError.Resolution.Cancel -> throw CancellationException("Operation cancelled")
154+
is Issue.UnknownError.Resolution.Skip -> throw IllegalStateException("canSkip = false")
155+
}
156+
}
157+
}
158+
break // Path is now clear, proceed to creation
159+
}
160+
161+
is Issue.PathAlreadyExists.Resolution.Overwrite -> {
162+
while (currentCoroutineContext().isActive) {
163+
try {
164+
destinationPath.deleteWalk(gatewaySwitch)
165+
break // Delete succeeded, exit loop
166+
} catch (e: Exception) {
167+
val deleteIssue = Issue.UnknownError(
168+
exception = e,
169+
errorMessage = (e.message ?: e.toString()).toCaString(),
170+
destination = destinationPath.lookup(gatewaySwitch),
171+
canRetry = true,
172+
canSkip = false,
173+
)
174+
when (issueHandler.handleIssue(context, deleteIssue) as Issue.UnknownError.Resolution) {
175+
is Issue.UnknownError.Resolution.Retry -> continue
176+
is Issue.UnknownError.Resolution.Cancel -> throw CancellationException("Operation cancelled")
177+
is Issue.UnknownError.Resolution.Skip -> throw IllegalStateException("canSkip = false")
178+
}
179+
}
180+
}
181+
break // Path is now clear, proceed to creation
182+
}
183+
184+
is Issue.PathAlreadyExists.Resolution.Merge -> throw IllegalArgumentException("Can't merge on create")
185+
186+
is Issue.PathAlreadyExists.Resolution.Cancel -> throw CancellationException("Operation cancelled")
187+
}
188+
}
189+
190+
// At this point, destinationPath should be conflict-free
191+
destinationPath = currentOperation.parentPath.child(currentOperation.name)
192+
193+
while (currentCoroutineContext().isActive) {
194+
try {
195+
when (operation.type) {
196+
ExplorerOperation.FileOp.Create.Type.FILE -> {
197+
gatewaySwitch.createFile(destinationPath)
198+
}
199+
ExplorerOperation.FileOp.Create.Type.FOLDER -> {
200+
gatewaySwitch.createDir(destinationPath)
201+
}
202+
}
203+
break // Creation succeeded, exit loop
204+
} catch (e: Exception) {
205+
val createIssue = Issue.UnknownError(
206+
exception = e,
207+
errorMessage = (e.message ?: e.toString()).toCaString(),
208+
destination = destinationPath.lookup(gatewaySwitch),
209+
canRetry = true,
210+
canSkip = true,
211+
)
212+
when (issueHandler.handleIssue(context, createIssue) as Issue.UnknownError.Resolution) {
213+
is Issue.UnknownError.Resolution.Retry -> continue
214+
is Issue.UnknownError.Resolution.Cancel -> throw CancellationException("Operation cancelled")
215+
is Issue.UnknownError.Resolution.Skip -> return
216+
}
217+
}
218+
}
85219

86220
OperationNotifier.Hint.FilesAdded(
87221
operationId = operation.operationId,
88222
affectedFolder = operation.parentPath,
89-
files = listOf(targetPath),
223+
files = listOf(destinationPath),
90224
).run { emit(this) }
91225
}
92226

0 commit comments

Comments
 (0)