@@ -49,91 +49,89 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
49
49
return file.uri.toString() to os
50
50
}
51
51
52
- private fun openWriterFD ( fileName : String , offset : Long ): Pair < String , SeekableOutputStream ?> {
53
-
54
- val ctx = appContext ? : return " " to null
55
- val dirUri = savedUri ? : return " " to null
56
- val dir = DocumentFile .fromTreeUri(ctx, Uri .parse(dirUri)) ? : return " " to null
57
-
58
- // Reuse existing doc if it exists
52
+ @Throws( IOException :: class )
53
+ private fun openWriterFD ( fileName : String , offset : Long ): Pair < String , SeekableOutputStream > {
54
+ val ctx = appContext ? : throw IOException ( " App context not initialized " )
55
+ val dirUri = savedUri ? : throw IOException ( " No directory URI " )
56
+ val dir =
57
+ DocumentFile .fromTreeUri(ctx, Uri .parse(dirUri))
58
+ ? : throw IOException ( " Invalid tree URI: $dirUri " )
59
59
val file =
60
60
dir.findFile(fileName)
61
61
? : dir.createFile(" application/octet-stream" , fileName)
62
- ? : return " " to null
62
+ ? : throw IOException ( " Failed to create file: $fileName " )
63
63
64
- // Always get a ParcelFileDescriptor so we can sync
65
- val pfd = ctx.contentResolver.openFileDescriptor(file.uri, " rw" ) ? : return " " to null
64
+ val pfd =
65
+ ctx.contentResolver.openFileDescriptor(file.uri, " rw" )
66
+ ? : throw IOException (" Failed to open file descriptor for ${file.uri} " )
66
67
val fos = FileOutputStream (pfd.fileDescriptor)
67
68
68
69
if (offset != 0L ) fos.channel.position(offset) else fos.channel.truncate(0 )
69
-
70
70
return file.uri.toString() to SeekableOutputStream (fos, pfd)
71
71
}
72
72
73
73
private val currentUri = ConcurrentHashMap <String , String >()
74
74
75
- override fun openFileWriter (fileName : String ): libtailscale.OutputStream {
76
- val (uri, stream) = openWriterFD(fileName, 0 )
77
- currentUri[fileName] = uri // 🠚 cache the exact doc we opened
78
- return OutputStreamAdapter (stream ? : OutputStream .nullOutputStream())
79
- }
80
-
81
- override fun openFileWriterAt (fileName : String , offset : Long ): libtailscale.OutputStream {
75
+ @Throws(IOException ::class )
76
+ override fun openFileWriter (fileName : String , offset : Long ): libtailscale.OutputStream {
82
77
val (uri, stream) = openWriterFD(fileName, offset)
78
+ if (stream == null ) {
79
+ throw IOException (" Failed to open file writer for $fileName " )
80
+ }
83
81
currentUri[fileName] = uri
84
- return OutputStreamAdapter (stream ? : OutputStream .nullOutputStream() )
82
+ return OutputStreamAdapter (stream)
85
83
}
86
84
87
- override fun openFileURI (fileName : String ): String {
85
+ @Throws(IOException ::class )
86
+ override fun getFileURI (fileName : String ): String {
88
87
currentUri[fileName]?.let {
89
88
return it
90
89
}
91
- val ctx = appContext ? : return " "
92
- val dirStr = savedUri ? : return " "
93
- val dir = DocumentFile .fromTreeUri(ctx, Uri .parse(dirStr)) ? : return " "
94
90
95
- val file = dir.findFile(fileName) ? : return " "
96
- val uri = file.uri.toString()
91
+ val ctx = appContext ? : throw IOException (" App context not initialized" )
92
+ val dirStr = savedUri ? : throw IOException (" No saved directory URI" )
93
+ val dir =
94
+ DocumentFile .fromTreeUri(ctx, Uri .parse(dirStr))
95
+ ? : throw IOException (" Invalid tree URI: $dirStr " )
97
96
97
+ val file = dir.findFile(fileName) ? : throw IOException (" File not found: $fileName " )
98
+ val uri = file.uri.toString()
98
99
currentUri[fileName] = uri
99
100
return uri
100
101
}
101
102
102
- override fun renamePartialFile (
103
- partialUri : String ,
104
- targetDirUri : String ,
105
- targetName : String
106
- ): String {
103
+ @Throws(IOException ::class )
104
+ override fun renameFile (oldPath : String , targetName : String ): String {
107
105
val ctx = appContext ? : throw IOException (" not initialized" )
108
- val srcUri = Uri .parse(partialUri)
106
+ val dirUri = savedUri ? : throw IOException (" directory not set" )
107
+ val srcUri = Uri .parse(oldPath)
109
108
val dir =
110
- DocumentFile .fromTreeUri(ctx, Uri .parse(targetDirUri ))
111
- ? : throw IOException (" cannot open dir $targetDirUri " )
112
-
109
+ DocumentFile .fromTreeUri(ctx, Uri .parse(dirUri ))
110
+ ? : throw IOException (" cannot open dir $dirUri " )
111
+
113
112
var finalName = targetName
114
113
dir.findFile(finalName)?.let { existing ->
115
114
if (lengthOfUri(ctx, existing.uri) == 0L ) {
116
- existing.delete() // remove stale 0‑byte file
115
+ existing.delete()
117
116
} else {
118
117
finalName = generateNewFilename(finalName)
119
118
}
120
119
}
121
-
120
+
122
121
try {
123
122
DocumentsContract .renameDocument(ctx.contentResolver, srcUri, finalName)?.also { newUri ->
124
123
runCatching { ctx.contentResolver.delete(srcUri, null , null ) }
125
124
cleanupPartials(dir, targetName)
126
125
return newUri.toString()
127
126
}
128
127
} catch (_: Exception ) {
129
- // rename not supported; fall through to copy‑delete
128
+ // fallback
130
129
}
131
-
132
- // fallback - copy contents then delete source
130
+
133
131
val dest =
134
132
dir.createFile(" application/octet-stream" , finalName)
135
133
? : throw IOException (" createFile failed for $finalName " )
136
-
134
+
137
135
ctx.contentResolver.openInputStream(srcUri).use { inp ->
138
136
ctx.contentResolver.openOutputStream(dest.uri, " w" ).use { out ->
139
137
if (inp == null || out == null ) {
@@ -143,7 +141,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
143
141
inp.copyTo(out )
144
142
}
145
143
}
146
- // delete the original .partial
144
+
147
145
ctx.contentResolver.delete(srcUri, null , null )
148
146
cleanupPartials(dir, targetName)
149
147
return dest.uri.toString()
@@ -163,33 +161,35 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
163
161
}
164
162
165
163
@Throws(IOException ::class )
166
- override fun deleteFile (uriString : String ) {
164
+ override fun deleteFile (uri : String ) {
167
165
val ctx = appContext ? : throw IOException (" DeleteFile: not initialized" )
168
166
169
- val uri = Uri .parse(uriString )
167
+ val uri = Uri .parse(uri )
170
168
val doc =
171
169
DocumentFile .fromSingleUri(ctx, uri)
172
- ? : throw IOException (" DeleteFile: cannot resolve URI $uriString " )
170
+ ? : throw IOException (" DeleteFile: cannot resolve URI $uri " )
173
171
174
172
if (! doc.delete()) {
175
- throw IOException (" DeleteFile: delete() returned false for $uriString " )
173
+ throw IOException (" DeleteFile: delete() returned false for $uri " )
176
174
}
177
175
}
178
176
179
- override fun treeURI (): String = savedUri ? : throw IllegalStateException (" not initialized" )
180
-
177
+ @Throws(IOException ::class )
181
178
override fun getFileInfo (fileName : String ): String {
182
- val context = appContext ? : return " "
183
- val dirUri = savedUri ? : return " "
184
- val dir = DocumentFile .fromTreeUri(context, Uri .parse(dirUri)) ? : return " "
179
+ val context = appContext ? : throw IOException (" not initialized" )
180
+ val dirUri = savedUri ? : throw IOException (" not initialized" )
181
+ val dir =
182
+ DocumentFile .fromTreeUri(context, Uri .parse(dirUri))
183
+ ? : throw IOException (" could not resolve SAF root" )
185
184
186
- val file = dir.findFile(fileName) ? : return " "
185
+ val file =
186
+ dir.findFile(fileName) ? : throw IOException (" file \" $fileName \" not found in SAF directory" )
187
187
188
- val name = file.name ? : return " "
188
+ val name = file.name ? : throw IOException ( " file name missing for $fileName " )
189
189
val size = file.length()
190
- val modTime = file.lastModified() // milliseconds since epoch
190
+ val modTime = file.lastModified()
191
191
192
- return """ {"name":${jsonEscape (name)} ,"size":$size ,"modTime":$modTime }"""
192
+ return """ {"name":${JSONObject .quote (name)} ,"size":$size ,"modTime":$modTime }"""
193
193
}
194
194
195
195
private fun jsonEscape (s : String ): String {
@@ -216,71 +216,67 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
216
216
.toTypedArray()
217
217
}
218
218
219
- override fun listPartialFilesJSON (suffix : String ): String {
220
- return listPartialFiles(suffix)
221
- .joinToString(prefix = " [\" " , separator = " \" ,\" " , postfix = " \" ]" )
219
+ @Throws(IOException ::class )
220
+ override fun listFilesJSON (suffix : String ): String {
221
+ val list = listPartialFiles(suffix)
222
+ if (list.isEmpty()) {
223
+ throw IOException (" no files found matching suffix \" $suffix \" " )
224
+ }
225
+ return list.joinToString(prefix = " [\" " , separator = " \" ,\" " , postfix = " \" ]" )
222
226
}
223
227
224
- override fun openPartialFileReader (name : String ): libtailscale.InputStream ? {
225
- val context = appContext ? : return null
226
- val rootUri = savedUri ? : return null
227
- val dir = DocumentFile .fromTreeUri(context, Uri .parse(rootUri)) ? : return null
228
+ @Throws(IOException ::class )
229
+ override fun openFileReader (name : String ): libtailscale.InputStream {
230
+ val context = appContext ? : throw IOException (" not initialized" )
231
+ val rootUri = savedUri ? : throw IOException (" not initialized" )
232
+ val dir =
233
+ DocumentFile .fromTreeUri(context, Uri .parse(rootUri))
234
+ ? : throw IOException (" could not open SAF root" )
228
235
229
- // We know `name` includes the suffix (e.g. ".<id>.partial"), but the actual
230
- // file in SAF might include extra bits, so let's just match by that suffix.
231
- // You could also match exactly `endsWith(name)` if the filenames line up
232
- val suffix = name.substringAfterLast(' .' , " .$name " ) // or hard-code ".partial"
236
+ val suffix = name.substringAfterLast(' .' , " .$name " )
233
237
234
238
val file =
235
239
dir.listFiles().firstOrNull {
236
240
val fname = it.name ? : return @firstOrNull false
237
- // call the String overload explicitly:
238
- fname.endsWith(suffix, /* ignoreCase=*/ false )
239
- }
240
- ? : run {
241
- TSLog .d(" ShareFileHelper" , " no file ending with $suffix in SAF directory" )
242
- return null
243
- }
241
+ fname.endsWith(suffix, ignoreCase = false )
242
+ } ? : throw IOException (" no file ending with \" $suffix \" in SAF directory" )
244
243
245
- TSLog .d(" ShareFileHelper" , " found SAF file ${file.name} , opening" )
246
244
val inStream =
247
245
context.contentResolver.openInputStream(file.uri)
248
- ? : run {
249
- TSLog .d(" ShareFileHelper" , " openInputStream returned null for ${file.uri} " )
250
- return null
251
- }
246
+ ? : throw IOException (" openInputStream returned null for ${file.uri} " )
247
+
252
248
return InputStreamAdapter (inStream)
253
249
}
254
- }
255
250
256
- private class SeekableOutputStream (
257
- private val fos : FileOutputStream ,
258
- private val pfd : ParcelFileDescriptor
259
- ) : OutputStream() {
251
+ private class SeekableOutputStream (
252
+ private val fos : FileOutputStream ,
253
+ private val pfd : ParcelFileDescriptor
254
+ ) : OutputStream() {
260
255
261
- private var closed = false
256
+ private var closed = false
262
257
263
- override fun write (b : Int ) = fos.write(b)
258
+ override fun write (b : Int ) = fos.write(b)
264
259
265
- override fun write (b : ByteArray ) = fos.write(b)
260
+ override fun write (b : ByteArray ) = fos.write(b)
266
261
267
- override fun write (b : ByteArray , off : Int , len : Int ) {
268
- fos.write(b, off, len)
269
- }
262
+ override fun write (b : ByteArray , off : Int , len : Int ) {
263
+ fos.write(b, off, len)
264
+ }
270
265
271
- override fun close () {
272
- if (! closed) {
273
- closed = true
274
- try {
275
- fos.flush()
276
- fos.fd.sync() // blocks until data + metadata are durable
277
- val size = fos.channel.size()
278
- } finally {
279
- fos.close()
280
- pfd.close()
266
+ override fun close () {
267
+ if (! closed) {
268
+ closed = true
269
+ try {
270
+ fos.flush()
271
+ fos.fd.sync() // blocks until data + metadata are durable
272
+ val size = fos.channel.size()
273
+ } finally {
274
+ fos.close()
275
+ pfd.close()
276
+ }
281
277
}
282
278
}
283
- }
284
279
285
- override fun flush () = fos.flush()
280
+ override fun flush () = fos.flush()
281
+ }
286
282
}
0 commit comments