android: bump OSS

OSS and Version updated to 1.87.25-t0f15e4419-gde3b6dbfd

Signed-off-by: kari-ts <kari@tailscale.com>
pull/675/head
kari-ts 4 months ago
parent de3b6dbfd6
commit 3022490db6

@ -49,71 +49,70 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
return file.uri.toString() to os return file.uri.toString() to os
} }
private fun openWriterFD(fileName: String, offset: Long): Pair<String, SeekableOutputStream?> { @Throws(IOException::class)
private fun openWriterFD(fileName: String, offset: Long): Pair<String, SeekableOutputStream> {
val ctx = appContext ?: return "" to null val ctx = appContext ?: throw IOException("App context not initialized")
val dirUri = savedUri ?: return "" to null val dirUri = savedUri ?: throw IOException("No directory URI")
val dir = DocumentFile.fromTreeUri(ctx, Uri.parse(dirUri)) ?: return "" to null val dir =
DocumentFile.fromTreeUri(ctx, Uri.parse(dirUri))
// Reuse existing doc if it exists ?: throw IOException("Invalid tree URI: $dirUri")
val file = val file =
dir.findFile(fileName) dir.findFile(fileName)
?: dir.createFile("application/octet-stream", fileName) ?: dir.createFile("application/octet-stream", fileName)
?: return "" to null ?: throw IOException("Failed to create file: $fileName")
// Always get a ParcelFileDescriptor so we can sync val pfd =
val pfd = ctx.contentResolver.openFileDescriptor(file.uri, "rw") ?: return "" to null ctx.contentResolver.openFileDescriptor(file.uri, "rw")
?: throw IOException("Failed to open file descriptor for ${file.uri}")
val fos = FileOutputStream(pfd.fileDescriptor) val fos = FileOutputStream(pfd.fileDescriptor)
if (offset != 0L) fos.channel.position(offset) else fos.channel.truncate(0) if (offset != 0L) fos.channel.position(offset) else fos.channel.truncate(0)
return file.uri.toString() to SeekableOutputStream(fos, pfd) return file.uri.toString() to SeekableOutputStream(fos, pfd)
} }
private val currentUri = ConcurrentHashMap<String, String>() private val currentUri = ConcurrentHashMap<String, String>()
override fun openFileWriter(fileName: String): libtailscale.OutputStream { @Throws(IOException::class)
val (uri, stream) = openWriterFD(fileName, 0) override fun openFileWriter(fileName: String, offset: Long): libtailscale.OutputStream {
currentUri[fileName] = uri // 🠚 cache the exact doc we opened
return OutputStreamAdapter(stream ?: OutputStream.nullOutputStream())
}
override fun openFileWriterAt(fileName: String, offset: Long): libtailscale.OutputStream {
val (uri, stream) = openWriterFD(fileName, offset) val (uri, stream) = openWriterFD(fileName, offset)
if (stream == null) {
throw IOException("Failed to open file writer for $fileName")
}
currentUri[fileName] = uri currentUri[fileName] = uri
return OutputStreamAdapter(stream ?: OutputStream.nullOutputStream()) return OutputStreamAdapter(stream)
} }
override fun openFileURI(fileName: String): String { @Throws(IOException::class)
override fun getFileURI(fileName: String): String {
currentUri[fileName]?.let { currentUri[fileName]?.let {
return it return it
} }
val ctx = appContext ?: return ""
val dirStr = savedUri ?: return ""
val dir = DocumentFile.fromTreeUri(ctx, Uri.parse(dirStr)) ?: return ""
val file = dir.findFile(fileName) ?: return "" val ctx = appContext ?: throw IOException("App context not initialized")
val uri = file.uri.toString() val dirStr = savedUri ?: throw IOException("No saved directory URI")
val dir =
DocumentFile.fromTreeUri(ctx, Uri.parse(dirStr))
?: throw IOException("Invalid tree URI: $dirStr")
val file = dir.findFile(fileName) ?: throw IOException("File not found: $fileName")
val uri = file.uri.toString()
currentUri[fileName] = uri currentUri[fileName] = uri
return uri return uri
} }
override fun renamePartialFile( @Throws(IOException::class)
partialUri: String, override fun renameFile(oldPath: String, targetName: String): String {
targetDirUri: String,
targetName: String
): String {
val ctx = appContext ?: throw IOException("not initialized") val ctx = appContext ?: throw IOException("not initialized")
val srcUri = Uri.parse(partialUri) val dirUri = savedUri ?: throw IOException("directory not set")
val srcUri = Uri.parse(oldPath)
val dir = val dir =
DocumentFile.fromTreeUri(ctx, Uri.parse(targetDirUri)) DocumentFile.fromTreeUri(ctx, Uri.parse(dirUri))
?: throw IOException("cannot open dir $targetDirUri") ?: throw IOException("cannot open dir $dirUri")
var finalName = targetName var finalName = targetName
dir.findFile(finalName)?.let { existing -> dir.findFile(finalName)?.let { existing ->
if (lengthOfUri(ctx, existing.uri) == 0L) { if (lengthOfUri(ctx, existing.uri) == 0L) {
existing.delete() // remove stale 0byte file existing.delete()
} else { } else {
finalName = generateNewFilename(finalName) finalName = generateNewFilename(finalName)
} }
@ -125,11 +124,11 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
cleanupPartials(dir, targetName) cleanupPartials(dir, targetName)
return newUri.toString() return newUri.toString()
} }
} catch (_: Exception) { } catch (e: Exception) {
// rename not supported; fall through to copydelete TSLog.w("renameFile", "renameDocument fallback triggered for $srcUri -> $finalName: ${e.message}")
} }
// fallback - copy contents then delete source
val dest = val dest =
dir.createFile("application/octet-stream", finalName) dir.createFile("application/octet-stream", finalName)
?: throw IOException("createFile failed for $finalName") ?: throw IOException("createFile failed for $finalName")
@ -143,7 +142,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
inp.copyTo(out) inp.copyTo(out)
} }
} }
// delete the original .partial
ctx.contentResolver.delete(srcUri, null, null) ctx.contentResolver.delete(srcUri, null, null)
cleanupPartials(dir, targetName) cleanupPartials(dir, targetName)
return dest.uri.toString() return dest.uri.toString()
@ -163,33 +162,35 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
} }
@Throws(IOException::class) @Throws(IOException::class)
override fun deleteFile(uriString: String) { override fun deleteFile(uri: String) {
val ctx = appContext ?: throw IOException("DeleteFile: not initialized") val ctx = appContext ?: throw IOException("DeleteFile: not initialized")
val uri = Uri.parse(uriString) val uri = Uri.parse(uri)
val doc = val doc =
DocumentFile.fromSingleUri(ctx, uri) DocumentFile.fromSingleUri(ctx, uri)
?: throw IOException("DeleteFile: cannot resolve URI $uriString") ?: throw IOException("DeleteFile: cannot resolve URI $uri")
if (!doc.delete()) { if (!doc.delete()) {
throw IOException("DeleteFile: delete() returned false for $uriString") throw IOException("DeleteFile: delete() returned false for $uri")
} }
} }
override fun treeURI(): String = savedUri ?: throw IllegalStateException("not initialized") @Throws(IOException::class)
override fun getFileInfo(fileName: String): String { override fun getFileInfo(fileName: String): String {
val context = appContext ?: return "" val context = appContext ?: throw IOException("app context not initialized")
val dirUri = savedUri ?: return "" val dirUri = savedUri ?: throw IOException("SAF URI not initialized")
val dir = DocumentFile.fromTreeUri(context, Uri.parse(dirUri)) ?: return "" val dir =
DocumentFile.fromTreeUri(context, Uri.parse(dirUri))
?: throw IOException("could not resolve SAF root")
val file = dir.findFile(fileName) ?: return "" val file =
dir.findFile(fileName) ?: throw IOException("file \"$fileName\" not found in SAF directory")
val name = file.name ?: return "" val name = file.name ?: throw IOException("file name missing for $fileName")
val size = file.length() val size = file.length()
val modTime = file.lastModified() // milliseconds since epoch val modTime = file.lastModified()
return """{"name":${jsonEscape(name)},"size":$size,"modTime":$modTime}""" return """{"name":${JSONObject.quote(name)},"size":$size,"modTime":$modTime}"""
} }
private fun jsonEscape(s: String): String { private fun jsonEscape(s: String): String {
@ -216,42 +217,37 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
.toTypedArray() .toTypedArray()
} }
override fun listPartialFilesJSON(suffix: String): String { @Throws(IOException::class)
return listPartialFiles(suffix) override fun listFilesJSON(suffix: String): String {
.joinToString(prefix = "[\"", separator = "\",\"", postfix = "\"]") val list = listPartialFiles(suffix)
if (list.isEmpty()) {
throw IOException("no files found matching suffix \"$suffix\"")
}
return list.joinToString(prefix = "[\"", separator = "\",\"", postfix = "\"]")
} }
override fun openPartialFileReader(name: String): libtailscale.InputStream? { @Throws(IOException::class)
val context = appContext ?: return null override fun openFileReader(name: String): libtailscale.InputStream {
val rootUri = savedUri ?: return null val context = appContext ?: throw IOException("app context not initialized")
val dir = DocumentFile.fromTreeUri(context, Uri.parse(rootUri)) ?: return null val rootUri = savedUri ?: throw IOException("SAF URI not initialized")
val dir =
DocumentFile.fromTreeUri(context, Uri.parse(rootUri))
?: throw IOException("could not open SAF root")
// We know `name` includes the suffix (e.g. ".<id>.partial"), but the actual val suffix = name.substringAfterLast('.', ".$name")
// file in SAF might include extra bits, so let's just match by that suffix.
// You could also match exactly `endsWith(name)` if the filenames line up
val suffix = name.substringAfterLast('.', ".$name") // or hard-code ".partial"
val file = val file =
dir.listFiles().firstOrNull { dir.listFiles().firstOrNull {
val fname = it.name ?: return@firstOrNull false val fname = it.name ?: return@firstOrNull false
// call the String overload explicitly: fname.endsWith(suffix, ignoreCase = false)
fname.endsWith(suffix, /*ignoreCase=*/ false) } ?: throw IOException("no file ending with \"$suffix\" in SAF directory")
}
?: run {
TSLog.d("ShareFileHelper", "no file ending with $suffix in SAF directory")
return null
}
TSLog.d("ShareFileHelper", "found SAF file ${file.name}, opening")
val inStream = val inStream =
context.contentResolver.openInputStream(file.uri) context.contentResolver.openInputStream(file.uri)
?: run { ?: throw IOException("openInputStream returned null for ${file.uri}")
TSLog.d("ShareFileHelper", "openInputStream returned null for ${file.uri}")
return null
}
return InputStreamAdapter(inStream) return InputStreamAdapter(inStream)
} }
}
private class SeekableOutputStream( private class SeekableOutputStream(
private val fos: FileOutputStream, private val fos: FileOutputStream,
@ -274,7 +270,6 @@ private class SeekableOutputStream(
try { try {
fos.flush() fos.flush()
fos.fd.sync() // blocks until data + metadata are durable fos.fd.sync() // blocks until data + metadata are durable
val size = fos.channel.size()
} finally { } finally {
fos.close() fos.close()
pfd.close() pfd.close()
@ -284,3 +279,4 @@ private class SeekableOutputStream(
override fun flush() = fos.flush() override fun flush() = fos.flush()
} }
}

@ -3,9 +3,9 @@ module github.com/tailscale/tailscale-android
go 1.24.4 go 1.24.4
require ( require (
github.com/tailscale/wireguard-go v0.0.0-20250530210235-65cd6eed7d7f github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da
golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab
tailscale.com v1.85.0-pre.0.20250627205655-0a64e86a0df8 tailscale.com v1.87.0-pre.0.20250801224156-0f15e4419683
) )
require ( require (

@ -163,8 +163,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
github.com/tailscale/wireguard-go v0.0.0-20250530210235-65cd6eed7d7f h1:vg3PmQdq1BbB2V81iC1VBICQtfwbVGZ/4A/p7QKXTK0= github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
github.com/tailscale/wireguard-go v0.0.0-20250530210235-65cd6eed7d7f/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
@ -235,5 +235,5 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
tailscale.com v1.85.0-pre.0.20250627205655-0a64e86a0df8 h1:gR3XF35IWpV4WhON27gR2vd8ypXbnjnrj5WreLWFxWk= tailscale.com v1.87.0-pre.0.20250801224156-0f15e4419683 h1:meEUX1Nsr5SaXiaeivOGG4c7gsQm/P3Jr3dzbtE0j6k=
tailscale.com v1.85.0-pre.0.20250627205655-0a64e86a0df8/go.mod h1:zrtwlwmFfEWbUz77UN58gaLADx4rXSecFhGO+XW0JbU= tailscale.com v1.87.0-pre.0.20250801224156-0f15e4419683/go.mod h1:Lm8dnzU2i/Emw15r6sl3FRNp/liSQ/nYw6ZSQvIdZ1M=

@ -337,7 +337,7 @@ func (a *App) newBackend(dataDir string, appCtx AppContext, store *stateStore,
} }
lb, err := ipnlocal.NewLocalBackend(logf, logID.Public(), sys, 0) lb, err := ipnlocal.NewLocalBackend(logf, logID.Public(), sys, 0)
if ext, ok := ipnlocal.GetExt[*taildrop.Extension](lb); ok { if ext, ok := ipnlocal.GetExt[*taildrop.Extension](lb); ok {
ext.SetFileOps(NewAndroidFileOps(a.shareFileHelper)) ext.SetFileOps(newAndroidFileOps(a.shareFileHelper))
ext.SetDirectFileRoot(a.directFileRoot) ext.SetDirectFileRoot(a.directFileRoot)
} }

@ -4,7 +4,6 @@ package libtailscale
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"os" "os"
"time" "time"
@ -12,47 +11,47 @@ import (
"tailscale.com/feature/taildrop" "tailscale.com/feature/taildrop"
) )
// AndroidFileOps implements the FileOps interface using the Android ShareFileHelper. // androidFileOps implements [taildrop.FileOps] using the Android ShareFileHelper.
type AndroidFileOps struct { type androidFileOps struct {
helper ShareFileHelper helper ShareFileHelper
} }
var _ taildrop.FileOps = (*AndroidFileOps)(nil) var _ taildrop.FileOps = (*androidFileOps)(nil)
func NewAndroidFileOps(helper ShareFileHelper) *AndroidFileOps { func newAndroidFileOps(helper ShareFileHelper) *androidFileOps {
return &AndroidFileOps{helper: helper} return &androidFileOps{helper: helper}
} }
func (ops *AndroidFileOps) OpenWriter(name string, offset int64, perm os.FileMode) (io.WriteCloser, string, error) { func (ops *androidFileOps) OpenWriter(name string, offset int64, _ os.FileMode) (io.WriteCloser, string, error) {
var wc OutputStream wc, err := ops.helper.OpenFileWriter(name, offset)
if offset == 0 { if err != nil {
wc = ops.helper.OpenFileWriter(name) return nil, "", err
} else {
wc = ops.helper.OpenFileWriterAt(name, offset)
} }
if wc == nil { uri, err := ops.helper.GetFileURI(name)
return nil, "", fmt.Errorf("OpenFileWriter returned nil for %q", name) if err != nil {
wc.Close()
return nil, "", err
} }
uri := ops.helper.OpenFileURI(name)
return wc, uri, nil return wc, uri, nil
} }
func (ops *AndroidFileOps) Remove(baseName string) error { func (ops *androidFileOps) Remove(baseName string) error {
uri := ops.helper.OpenFileURI(baseName) uri, err := ops.helper.GetFileURI(baseName)
if err != nil {
return err
}
return ops.helper.DeleteFile(uri) return ops.helper.DeleteFile(uri)
} }
func (ops *AndroidFileOps) Rename(oldPath, newName string) (string, error) { func (ops *androidFileOps) Rename(oldPath, newName string) (string, error) {
tree := ops.helper.TreeURI() return ops.helper.RenameFile(oldPath, newName)
newURI := ops.helper.RenamePartialFile(oldPath, tree, newName)
if newURI == "" {
return "", fmt.Errorf("failed to rename partial file via SAF")
}
return newURI, nil
} }
func (ops *AndroidFileOps) ListFiles() ([]string, error) { func (ops *androidFileOps) ListFiles() ([]string, error) {
namesJSON := ops.helper.ListPartialFilesJSON("") namesJSON, err := ops.helper.ListFilesJSON("")
if err != nil {
return nil, err
}
var names []string var names []string
if err := json.Unmarshal([]byte(namesJSON), &names); err != nil { if err := json.Unmarshal([]byte(namesJSON), &names); err != nil {
return nil, err return nil, err
@ -60,43 +59,42 @@ func (ops *AndroidFileOps) ListFiles() ([]string, error) {
return names, nil return names, nil
} }
func (ops *AndroidFileOps) OpenReader(name string) (io.ReadCloser, error) { func (ops *androidFileOps) OpenReader(name string) (io.ReadCloser, error) {
in := ops.helper.OpenPartialFileReader(name) in, err := ops.helper.OpenFileReader(name)
if in == nil { if err != nil {
return nil, fmt.Errorf("OpenPartialFileReader returned nil for %q", name) return nil, err
} }
return adaptInputStream(in), nil return adaptInputStream(in), nil
} }
func (ops *AndroidFileOps) Stat(name string) (os.FileInfo, error) { func (ops *androidFileOps) Stat(name string) (os.FileInfo, error) {
infoJSON := ops.helper.GetFileInfo(name) infoJSON, err := ops.helper.GetFileInfo(name)
if infoJSON == "" { if err != nil {
return nil, os.ErrNotExist return nil, err
}
var info struct {
Name string `json:"name"`
Size int64 `json:"size"`
ModTime int64 `json:"modTime"` // Unix millis
} }
if err := json.Unmarshal([]byte(infoJSON), &info); err != nil { var fi androidFileInfo
if err := json.Unmarshal([]byte(infoJSON), &fi); err != nil {
return nil, err return nil, err
} }
return &androidFileInfo{ return &fi, nil
name: info.Name, }
size: info.Size,
modTime: time.UnixMilli(info.ModTime), type androidFileInfoJSON struct {
}, nil Name string `json:"name"`
Size int64 `json:"size"`
ModTime int64 `json:"modTime"`
} }
type androidFileInfo struct { type androidFileInfo struct {
name string data androidFileInfoJSON
size int64
modTime time.Time
} }
func (fi *androidFileInfo) Name() string { return fi.name } // compile-time check
func (fi *androidFileInfo) Size() int64 { return fi.size } var _ os.FileInfo = (*androidFileInfo)(nil)
func (fi *androidFileInfo) Name() string { return fi.data.Name }
func (fi *androidFileInfo) Size() int64 { return fi.data.Size }
func (fi *androidFileInfo) Mode() os.FileMode { return 0o600 } func (fi *androidFileInfo) Mode() os.FileMode { return 0o600 }
func (fi *androidFileInfo) ModTime() time.Time { return fi.modTime } func (fi *androidFileInfo) ModTime() time.Time { return time.UnixMilli(fi.data.ModTime) }
func (fi *androidFileInfo) IsDir() bool { return false } func (fi *androidFileInfo) IsDir() bool { return false }
func (fi *androidFileInfo) Sys() interface{} { return nil } func (fi *androidFileInfo) Sys() any { return nil }

@ -175,45 +175,36 @@ type OutputStream interface {
// ShareFileHelper corresponds to the Kotlin ShareFileHelper class // ShareFileHelper corresponds to the Kotlin ShareFileHelper class
type ShareFileHelper interface { type ShareFileHelper interface {
// OpenFileWriter creates or truncates a file named fileName // OpenFileWriter creates or truncates a file named fileName at a given offset,
// and returns an OutputStream for writing to it from the beginning. // returning an OutputStream for writing. Returns an error if the file cannot be opened.
// Returns nil if the file cannot be opened. OpenFileWriter(fileName string, offset int64) (stream OutputStream, err error)
OpenFileWriter(fileName string) OutputStream
// GetFileURI returns the SAF URI string for the file named fileName,
// OpenFileWriterAt opens fileName for writing at a given offset. // or an error if the file cannot be resolved.
// Returns nil if the file cannot be opened. GetFileURI(fileName string) (uri string, err error)
OpenFileWriterAt(fileName string, offset int64) OutputStream
// RenameFile renames the file at oldPath (a SAF URI) into the Taildrop directory,
// OpenFileURI returns the SAF URI string for the file named fileName, // giving it the new targetName. Returns the SAF URI of the renamed file, or an error.
// or an empty string if the file cannot be resolved. RenameFile(oldPath string, targetName string) (newURI string, err error)
OpenFileURI(fileName string) string
// ListFilesJSON returns a JSON-encoded list of filenames in the Taildrop directory
// RenamePartialFile renames the file at oldPath (a SAF URI) // that end with the specified suffix. If the suffix is empty, it returns all files.
// into the directory identified by newPath (a tree URI), // Returns an error if no matching files are found or the directory cannot be accessed.
// giving it the new targetName. Returns the SAF URI of the renamed file, ListFilesJSON(suffix string) (json string, err error)
// or an empty string if the operation failed.
RenamePartialFile(oldPath string, newPath string, targetName string) string // OpenFileReader opens the file with the given name (typically a .partial file)
// ListPartialFilesJSON returns a JSON-encoded list of partial filenames
// (e.g., ["foo.partial", "bar.partial"]) that match the given suffix.
ListPartialFilesJSON(suffix string) string
// OpenPartialFileReader opens the file with the given name (typically a .partial file)
// and returns an InputStream for reading its contents. // and returns an InputStream for reading its contents.
// Returns nil if the file cannot be opened. // Returns an error if the file cannot be opened.
OpenPartialFileReader(name string) InputStream OpenFileReader(name string) (stream InputStream, err error)
// DeleteFile deletes the file identified by the given SAF URI string. // DeleteFile deletes the file identified by the given SAF URI string.
// Returns an error if the file could not be deleted. // Returns an error if the file could not be deleted.
DeleteFile(uriString string) error DeleteFile(uri string) error
// TreeURI returns the SAF tree URI representing the root directory for Taildrop files.
// This is typically the URI granted by the user via the Android directory picker.
TreeURI() string
// GetFileInfo returns a JSON-encoded string with file metadata for fileName. // GetFileInfo returns a JSON-encoded string containing metadata for fileName,
// Returns an empty string if the file does not exist or cannot be accessed. // matching the fields of androidFileInfo (name, size, modTime).
GetFileInfo(fileName string) string // Returns an error if the file does not exist or cannot be accessed.
GetFileInfo(fileName string) (json string, err error)
} }
// The below are global callbacks that allow the Java application to notify Go // The below are global callbacks that allow the Java application to notify Go

@ -8,7 +8,7 @@ import (
"log" "log"
) )
// adaptInputStream wraps a libtailscale.InputStream into an io.ReadCloser. // adaptInputStream wraps an [InputStream] into an [io.ReadCloser].
// It launches a goroutine to stream reads into a pipe. // It launches a goroutine to stream reads into a pipe.
func adaptInputStream(in InputStream) io.ReadCloser { func adaptInputStream(in InputStream) io.ReadCloser {
if in == nil { if in == nil {
@ -20,12 +20,16 @@ func adaptInputStream(in InputStream) io.ReadCloser {
for { for {
b, err := in.Read() b, err := in.Read()
if err != nil { if err != nil {
log.Printf("error reading from inputstream: %s", err) log.Printf("error reading from inputstream: %v", err)
return
} }
if b == nil { if b == nil {
return return
} }
w.Write(b) if _, err := w.Write(b); err != nil {
log.Printf("error writing to pipe: %v", err)
return
}
} }
}() }()
return r return r

Loading…
Cancel
Save