@@ -3,22 +3,27 @@ package eu.darken.butler.explorer.core.operations.handlers
33import dagger.assisted.Assisted
44import dagger.assisted.AssistedFactory
55import dagger.assisted.AssistedInject
6+ import eu.darken.butler.common.ca.toCaString
67import eu.darken.butler.common.coroutine.DispatcherProvider
8+ import eu.darken.butler.common.debug.logging.Logging.Priority.*
79import eu.darken.butler.common.debug.logging.log
810import eu.darken.butler.common.debug.logging.logTag
11+ import eu.darken.butler.common.files.APath
912import eu.darken.butler.common.files.GatewaySwitch
10- import eu.darken.butler.common.files.extensions.copyOperation
1113import eu.darken.butler.common.files.extensions.deleteWalk
1214import eu.darken.butler.common.files.extensions.exists
1315import eu.darken.butler.common.files.extensions.lookup
1416import eu.darken.butler.common.files.operations.Issue
17+ import eu.darken.butler.common.files.operations.MoveOperation
1518import eu.darken.butler.explorer.core.engine.ExplorerOperation
1619import eu.darken.butler.explorer.core.operations.IssueHandler
1720import eu.darken.butler.explorer.core.operations.OperationContext
1821import eu.darken.butler.explorer.core.operations.OperationNotifier
1922import eu.darken.butler.workspace.core.Workspace
2023import kotlinx.coroutines.CancellationException
24+ import kotlinx.coroutines.currentCoroutineContext
2125import kotlinx.coroutines.flow.last
26+ import kotlinx.coroutines.isActive
2227
2328class 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