ipn,ipnlocal,taildrop: use SAF for Android files (#15976)

Create FileOps for calling platform-specific file operations such as SAF APIs in Taildrop
Update taildrop.PutFile to support both traditional and SAF modes

Updates tailscale/tailscale#15263

Signed-off-by: kari-ts <kari@tailscale.com>
This commit is contained in:
kari-ts
2025-05-20 15:30:19 -07:00
committed by GitHub
parent 70b6e8ca98
commit 5a8b99e977
5 changed files with 371 additions and 84 deletions

View File

@@ -73,6 +73,10 @@ type Extension struct {
// *.partial file to its final name on completion.
directFileRoot string
// FileOps abstracts platform-specific file operations needed for file transfers.
// This is currently being used for Android to use the Storage Access Framework.
FileOps FileOps
nodeBackendForTest ipnext.NodeBackend // if non-nil, pretend we're this node state for tests
mu sync.Mutex // Lock order: lb.mu > e.mu
@@ -85,6 +89,30 @@ type Extension struct {
outgoingFiles map[string]*ipn.OutgoingFile
}
// safDirectoryPrefix is used to determine if the directory is managed via SAF.
const SafDirectoryPrefix = "content://"
// PutMode controls how Manager.PutFile writes files to storage.
//
// PutModeDirect write files directly to a filesystem path (default).
// PutModeAndroidSAF use Androids Storage Access Framework (SAF), where
// the OS manages the underlying directory permissions.
type PutMode int
const (
PutModeDirect PutMode = iota
PutModeAndroidSAF
)
// FileOps defines platform-specific file operations.
type FileOps interface {
OpenFileWriter(filename string) (io.WriteCloser, string, error)
// RenamePartialFile finalizes a partial file.
// It returns the new SAF URI as a string and an error.
RenamePartialFile(partialUri, targetDirUri, targetName string) (string, error)
}
func (e *Extension) Name() string {
return "taildrop"
}
@@ -153,12 +181,18 @@ func (e *Extension) onChangeProfile(profile ipn.LoginProfileView, _ ipn.PrefsVie
if fileRoot == "" {
e.logf("no Taildrop directory configured")
}
mode := PutModeDirect
if e.directFileRoot != "" && strings.HasPrefix(e.directFileRoot, SafDirectoryPrefix) {
mode = PutModeAndroidSAF
}
e.setMgrLocked(managerOptions{
Logf: e.logf,
Clock: tstime.DefaultClock{Clock: e.sb.Clock()},
State: e.stateStore,
Dir: fileRoot,
DirectFileMode: isDirectFileMode,
FileOps: e.FileOps,
Mode: mode,
SendFileNotify: e.sendFileNotify,
}.New())
}