Skip to content

Commit 645a4a1

Browse files
committed
Merge branch 'worktree-explorer'
# Conflicts: # app-workspace-explorer/src/main/java/eu/darken/butler/explorer/core/engine/BrowsingEngine.kt
2 parents 53af5a4 + 54e885d commit 645a4a1

19 files changed

Lines changed: 2475 additions & 437 deletions

File tree

EXPLORER_OPERATIONS_IMPLEMENTATION.md

Lines changed: 547 additions & 0 deletions
Large diffs are not rendered by default.

EXPLORER_REFACTOR_PLAN.md

Lines changed: 412 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package eu.darken.butler.common.files.extensions
2+
3+
import eu.darken.butler.common.debug.logging.Logging.Priority.WARN
4+
import eu.darken.butler.common.debug.logging.log
5+
import eu.darken.butler.common.files.APath
6+
import eu.darken.butler.common.files.APathGateway
7+
import eu.darken.butler.common.files.APathLookup
8+
import eu.darken.butler.common.files.APathLookupExtended
9+
import kotlinx.coroutines.flow.Flow
10+
import kotlinx.coroutines.flow.flow
11+
import okio.FileHandle
12+
import okio.IOException
13+
14+
data class CopyOperation(
15+
val state: State,
16+
val bytesCopied: Long = 0L,
17+
val totalBytes: Long = 0L,
18+
val currentPath: APath? = null,
19+
val result: APath? = null,
20+
val error: Exception? = null
21+
) {
22+
enum class State {
23+
CALCULATING_SIZE,
24+
COPYING,
25+
COMPLETED,
26+
FAILED
27+
}
28+
29+
val progress: Float = if (totalBytes > 0) bytesCopied.toFloat() / totalBytes else 0f
30+
}
31+
32+
fun <T : APath> T.copyOperation(
33+
gateway: APathGateway<T, out APathLookup<T>, out APathLookupExtended<T>>,
34+
target: T,
35+
overwrite: Boolean = false
36+
): Flow<CopyOperation> = flow {
37+
val source = this@copyOperation
38+
39+
// Emit calculating state
40+
emit(CopyOperation(state = CopyOperation.State.CALCULATING_SIZE))
41+
42+
// Check if source exists
43+
if (!source.exists(gateway)) {
44+
emit(CopyOperation(
45+
state = CopyOperation.State.FAILED,
46+
error = IOException("Source does not exist: $source")
47+
))
48+
return@flow
49+
}
50+
51+
val sourceLookup = gateway.lookup(source)
52+
53+
// Calculate total size
54+
val totalBytes = if (sourceLookup.isDirectory) {
55+
source.du(gateway)
56+
} else {
57+
sourceLookup.size
58+
}
59+
60+
emit(CopyOperation(
61+
state = CopyOperation.State.COPYING,
62+
totalBytes = totalBytes,
63+
currentPath = source
64+
))
65+
66+
try {
67+
// Handle directories
68+
if (sourceLookup.isDirectory) {
69+
source.copyDirectoryOperation(gateway, target, overwrite, totalBytes).collect { emit(it) }
70+
} else {
71+
// Handle files
72+
source.copyFileOperation(gateway, target, overwrite, totalBytes).collect { emit(it) }
73+
}
74+
75+
// Emit completion
76+
emit(CopyOperation(
77+
state = CopyOperation.State.COMPLETED,
78+
bytesCopied = totalBytes,
79+
totalBytes = totalBytes,
80+
result = target
81+
))
82+
} catch (e: Exception) {
83+
emit(CopyOperation(
84+
state = CopyOperation.State.FAILED,
85+
bytesCopied = 0L,
86+
totalBytes = totalBytes,
87+
error = e
88+
))
89+
}
90+
}
91+
92+
private fun <T : APath> T.copyFileOperation(
93+
gateway: APathGateway<T, out APathLookup<T>, out APathLookupExtended<T>>,
94+
target: T,
95+
overwrite: Boolean,
96+
totalBytes: Long
97+
): Flow<CopyOperation> = flow {
98+
if (!overwrite && target.exists(gateway)) {
99+
throw IOException("Target already exists: $target")
100+
}
101+
102+
// Create the target file
103+
target.createFileIfNecessary(gateway)
104+
105+
// Open source and target file handles
106+
val sourceHandle = gateway.file(this@copyFileOperation, readWrite = false)
107+
val targetHandle = gateway.file(target, readWrite = true)
108+
109+
sourceHandle.use { source ->
110+
targetHandle.use { target ->
111+
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
112+
var bytesCopied = 0L
113+
114+
while (true) {
115+
val bytesRead = source.read(bytesCopied, buffer, 0, buffer.size)
116+
if (bytesRead == -1) break
117+
118+
target.write(bytesCopied, buffer, 0, bytesRead)
119+
bytesCopied += bytesRead
120+
121+
// Emit progress
122+
emit(CopyOperation(
123+
state = CopyOperation.State.COPYING,
124+
bytesCopied = bytesCopied,
125+
totalBytes = totalBytes,
126+
currentPath = this@copyFileOperation
127+
))
128+
}
129+
}
130+
}
131+
132+
// Copy attributes
133+
try {
134+
val sourceLookup = gateway.lookup(this@copyFileOperation)
135+
val modifiedAt = sourceLookup.modifiedAt
136+
if (modifiedAt != null) {
137+
target.setModifiedAt(gateway, modifiedAt)
138+
}
139+
} catch (e: Exception) {
140+
log(WARN) { "Failed to copy attributes: ${e.message}" }
141+
}
142+
}
143+
144+
private fun <T : APath> T.copyDirectoryOperation(
145+
gateway: APathGateway<T, out APathLookup<T>, out APathLookupExtended<T>>,
146+
target: T,
147+
overwrite: Boolean,
148+
totalBytes: Long
149+
): Flow<CopyOperation> = flow {
150+
// Create target directory
151+
target.createDirIfNecessary(gateway)
152+
153+
var bytesCopied = 0L
154+
155+
// Copy all files in directory
156+
val files = gateway.listFiles(this@copyDirectoryOperation)
157+
for (file in files) {
158+
val targetFile = target.child(file.name) as T
159+
160+
@Suppress("UNCHECKED_CAST")
161+
(file as T).copyOperation(gateway, targetFile, overwrite)
162+
.collect { progress ->
163+
when (progress.state) {
164+
CopyOperation.State.COPYING -> {
165+
emit(CopyOperation(
166+
state = CopyOperation.State.COPYING,
167+
bytesCopied = bytesCopied + progress.bytesCopied,
168+
totalBytes = totalBytes,
169+
currentPath = progress.currentPath
170+
))
171+
}
172+
CopyOperation.State.COMPLETED -> {
173+
bytesCopied += progress.totalBytes
174+
}
175+
CopyOperation.State.FAILED -> throw progress.error!!
176+
else -> {} // Skip CALCULATING_SIZE
177+
}
178+
}
179+
}
180+
}
181+
182+
private const val DEFAULT_BUFFER_SIZE = 64 * 1024 // 64KB buffer

0 commit comments

Comments
 (0)