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