2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-30 13:47:41 +00:00

feat: add SAF support

This commit is contained in:
ShellWen Chen
2024-06-15 18:10:43 +08:00
committed by Albert Vaca Cintora
parent 7fbfc9df90
commit 0fb6e25682
7 changed files with 682 additions and 39 deletions

View File

@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
import java.io.IOException
import java.nio.file.attribute.BasicFileAttributeView
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileTime
object RootBasicFileAttributeView : BasicFileAttributeView {
override fun name(): String = "basic"
override fun readAttributes(): BasicFileAttributes = RootFileAttributes
override fun setTimes(
lastModifiedTime: FileTime?,
lastAccessTime: FileTime?,
createTime: FileTime?
) {
throw IOException("Set times of root directory is not supported")
}
}

View File

@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
import java.nio.file.attribute.FileTime
import java.nio.file.attribute.GroupPrincipal
import java.nio.file.attribute.PosixFileAttributes
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.UserPrincipal
object RootFileAttributes : PosixFileAttributes {
override fun lastModifiedTime(): FileTime = FileTime.fromMillis(0)
override fun lastAccessTime(): FileTime = FileTime.fromMillis(0)
override fun creationTime(): FileTime = FileTime.fromMillis(0)
override fun size(): Long = 0
override fun fileKey(): Any? = null
override fun isDirectory(): Boolean = true
override fun isRegularFile(): Boolean = false
override fun isSymbolicLink(): Boolean = false
override fun isOther(): Boolean = false
override fun owner(): UserPrincipal? = null
override fun group(): GroupPrincipal? = null
override fun permissions(): Set<PosixFilePermission> = // 660 for SAF
setOf(
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
)
}

View File

@@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
import java.io.IOException
import java.nio.file.attribute.FileTime
import java.nio.file.attribute.GroupPrincipal
import java.nio.file.attribute.PosixFileAttributeView
import java.nio.file.attribute.PosixFileAttributes
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.UserPrincipal
object RootPosixFileAttributeView : PosixFileAttributeView {
override fun name(): String = "posix"
override fun readAttributes(): PosixFileAttributes = RootFileAttributes
override fun setTimes(
lastModifiedTime: FileTime?,
lastAccessTime: FileTime?,
createTime: FileTime?
) {
throw IOException("Set times of root directory is not supported")
}
override fun getOwner(): UserPrincipal? = null
override fun setOwner(owner: UserPrincipal?) {
throw IOException("Set owner of root directory is not supported")
}
override fun setPermissions(perms: MutableSet<PosixFilePermission>?) {
throw IOException("Set permissions of root directory is not supported")
}
override fun setGroup(group: GroupPrincipal?) {
throw IOException("Set group of root directory is not supported")
}
}

View File

@@ -1,15 +1,21 @@
/*
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import org.apache.sshd.common.file.util.BaseFileSystem
import java.nio.file.attribute.UserPrincipalLookupService
import java.nio.file.spi.FileSystemProvider
class SafFileSystem(
fileSystemProvider: FileSystemProvider,
roots: MutableMap<String, String?>,
username: String,
private val roots: MutableMap<String, Uri>,
private val context: Context
) : BaseFileSystem<SafPath>(fileSystemProvider) {
override fun close() {
@@ -19,15 +25,52 @@ class SafFileSystem(
override fun isOpen(): Boolean = true
override fun supportedFileAttributeViews(): Set<String> = setOf("basic")
override fun supportedFileAttributeViews(): Set<String> = setOf("basic", "posix")
override fun getUserPrincipalLookupService(): UserPrincipalLookupService {
throw UnsupportedOperationException("SAF does not support user principal lookup")
}
override fun create(root: String, names: List<String>): SafPath {
private tailrec fun getDocumentFileFromPath(
docFile: DocumentFile,
names: List<String>
): DocumentFile? {
if (names.isEmpty()) {
return docFile
}
val nextName = names.first()
val nextNames = names.drop(1)
val nextDocFile = docFile.findFile(nextName)
return if (nextDocFile != null) {
getDocumentFileFromPath(nextDocFile, nextNames)
} else {
null
}
}
override fun create(root: String?, names: List<String>): SafPath {
Log.v(TAG, "create: $root, $names")
return SafPath(this, root, names)
if ((root == "/") && names.isEmpty()) {
return SafPath.newRootPath(this)
}
val dirName = names.getOrNull(0)
if (dirName != null) {
roots.forEach { (k, v) ->
if (k == dirName) {
if (names.size == 1) {
return SafPath(this, v, root, names)
} else {
val docFile = getDocumentFileFromPath(
DocumentFile.fromTreeUri(context, v)!!,
names.drop(1)
)
return SafPath(this, docFile?.uri, root, names)
}
}
}
}
return SafPath(this, null, root, names)
}
companion object {

View File

@@ -7,6 +7,7 @@
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
import android.content.Context
import android.net.Uri
import android.util.Log
import org.apache.sshd.common.file.FileSystemFactory
import org.apache.sshd.common.session.SessionContext
@@ -15,8 +16,8 @@ import java.nio.file.FileSystem
import java.nio.file.Path
class SafFileSystemFactory(private val context: Context) : FileSystemFactory {
private val provider = SafFileSystemProvider()
private val roots: MutableMap<String, String?> = HashMap()
private val roots: MutableMap<String, Uri> = HashMap()
private val provider = SafFileSystemProvider(context, roots)
fun initRoots(storageInfoList: List<SftpPlugin.StorageInfo>) {
Log.i(TAG, "initRoots: $storageInfoList")
@@ -29,9 +30,11 @@ class SafFileSystemFactory(private val context: Context) : FileSystemFactory {
// roots[curStorageInfo.displayName] = curStorageInfo.uri.path
// }
}
curStorageInfo.isContentUri -> {
roots[curStorageInfo.displayName] = curStorageInfo.uri.toString()
roots[curStorageInfo.displayName] = curStorageInfo.uri
}
else -> {
Log.e(TAG, "Unknown storage URI type: $curStorageInfo")
}
@@ -40,7 +43,7 @@ class SafFileSystemFactory(private val context: Context) : FileSystemFactory {
}
override fun createFileSystem(session: SessionContext?): FileSystem {
return SafFileSystem(provider, roots, session!!.username, context)
return SafFileSystem(provider, roots, context)
}
companion object {

View File

@@ -1,88 +1,433 @@
/*
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.util.Log
import java.io.FileNotFoundException
import java.io.IOException
import java.net.URI
import java.nio.channels.FileChannel
import java.nio.channels.SeekableByteChannel
import java.nio.file.AccessMode
import java.nio.file.CopyOption
import java.nio.file.DirectoryStream
import java.nio.file.FileStore
import java.nio.file.FileSystem
import java.nio.file.Files
import java.nio.file.LinkOption
import java.nio.file.OpenOption
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.nio.file.attribute.BasicFileAttributeView
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileAttribute
import java.nio.file.attribute.FileAttributeView
import java.nio.file.attribute.FileTime
import java.nio.file.attribute.GroupPrincipal
import java.nio.file.attribute.PosixFileAttributeView
import java.nio.file.attribute.PosixFileAttributes
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.UserPrincipal
import java.nio.file.spi.FileSystemProvider
class SafFileSystemProvider: FileSystemProvider() {
class SafFileSystemProvider(
private val context: Context,
val roots: MutableMap<String, Uri>
) : FileSystemProvider() {
override fun getScheme(): String = "saf"
override fun newFileSystem(uri: URI, env: MutableMap<String, *>?): FileSystem {
TODO("Not yet implemented")
// SSHD Core does not use this method, so we can just throw an exception
Log.w(TAG, "newFileSystem($uri) not implemented")
throw NotImplementedError("newFileSystem($uri) not implemented")
}
override fun getFileSystem(uri: URI): FileSystem {
TODO("Not yet implemented")
// SSHD Core does not use this method, so we can just throw an exception
Log.w(TAG, "getFileSystem($uri) not implemented")
throw NotImplementedError("getFileSystem($uri) not implemented")
}
override fun getPath(uri: URI): Path {
TODO("Not yet implemented")
// SSHD Core does not use this method, so we can just throw an exception
Log.w(TAG, "getPath($uri) not implemented")
throw NotImplementedError("getPath($uri) not implemented")
}
/**
* @see org.apache.sshd.sftp.server.FileHandle.getOpenOptions
*/
override fun newByteChannel(
path: Path,
options: MutableSet<out OpenOption>,
vararg attrs: FileAttribute<*>
options: Set<OpenOption>,
vararg attrs_: FileAttribute<*>
): SeekableByteChannel {
TODO("Not yet implemented")
return newFileChannel(path, options, *attrs_)
}
/**
* @see org.apache.sshd.sftp.server.FileHandle.getOpenOptions
*/
override fun newFileChannel(
path: Path,
options: Set<OpenOption>,
vararg attrs_: FileAttribute<*>
): FileChannel {
check(path is SafPath)
check(!path.isRoot())
/*
* According to https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-33
*
* The 'attrs' field is ignored if an existing file is opened.
*/
val attrs = if (Files.exists(path)) {
emptyArray()
} else {
attrs_
}
when (options) {
// READ
setOf(StandardOpenOption.READ) -> {
val docFile = path.getDocumentFile(context)!!
return ParcelFileDescriptor.AutoCloseInputStream(
context.contentResolver.openFileDescriptor(docFile.uri, "r")!!
).channel
}
// WRITE, TRUNCATE_EXISTING
// WRITE, CREATE
// WRITE, TRUNCATE_EXISTING, CREATE
// WRITE, CREATE_NEW
setOf(
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING,
),
setOf(
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
),
setOf(
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.CREATE
),
setOf(
StandardOpenOption.WRITE,
StandardOpenOption.CREATE_NEW,
) -> {
val docFile = path.getDocumentFile(context) ?: run {
if (options.contains(StandardOpenOption.CREATE) || options.contains(StandardOpenOption.CREATE_NEW)) {
val parent = path.parent.getDocumentFile(context)!!
parent.createFile(Files.probeContentType(path), path.names.last()).apply {
if (this == null) {
throw IOException("Failed to create $path")
}
path.safUri = uri
}
} else {
throw IOException("File not found: $path")
}
} ?: throw IOException("Failed to create $path")
check(docFile.exists())
return ParcelFileDescriptor.AutoCloseOutputStream(
context.contentResolver.openFileDescriptor(docFile.uri, "rw")!!
).channel
}
// TODO: Implement other options
}
Log.w(TAG, "newFileChannel($path, $options, $attrs) not implemented")
throw IOException("newFileChannel($path, $options, $attrs) not implemented")
}
override fun newDirectoryStream(
dir: Path,
filter: DirectoryStream.Filter<in Path>
): DirectoryStream<Path> {
TODO("Not yet implemented")
check(dir is SafPath)
if (dir.isRoot()) {
return object : DirectoryStream<Path> {
override fun iterator(): MutableIterator<Path> {
return roots.mapNotNull { (name, uri) ->
val newPath = SafPath(dir.fileSystem, uri, null, listOf(name))
if (filter.accept(newPath)) newPath else null
}.toMutableList().iterator()
}
override fun close() {
// no-op
}
}
}
check(dir.names.isNotEmpty())
return object : DirectoryStream<Path> {
override fun iterator(): MutableIterator<Path> {
val documentFile = dir.getDocumentFile(context)!!
return documentFile.listFiles().mapNotNull {
val newPath = SafPath(dir.fileSystem, it.uri, null, dir.names + it.name!!)
if (filter.accept(newPath)) newPath else null
}.toMutableList().iterator()
}
override fun close() {
// no-op
}
}
}
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
TODO("Not yet implemented")
check(dir is SafPath)
if (dir.isRoot()) {
throw IOException("Cannot create root directory")
}
if (dir.parent == null) {
throw IOException("Parent directory does not exist")
}
val parent = dir.parent.getDocumentFile(context)
?: throw IOException("Parent directory does not exist")
parent.createDirectory(dir.names.last())
}
override fun delete(path: Path) {
TODO("Not yet implemented")
check(path is SafPath)
val docFile = path.getDocumentFile(context)
?: throw java.nio.file.NoSuchFileException(
path.toString(),
) // No kotlin.NoSuchFileException, they are different
if (!docFile.delete()) {
throw IOException("Failed to delete $path")
}
}
override fun copy(source: Path, target: Path, vararg options: CopyOption) {
TODO("Not yet implemented")
check(source is SafPath)
check(target is SafPath)
val sourceDocFile = source.getDocumentFile(context)
?: throw java.nio.file.NoSuchFileException(
source.toString(),
) // No kotlin.NoSuchFileException, they are different
val targetDocFile = target.parent.getDocumentFile(context)!!
.createFile(Files.probeContentType(source), target.names.last())
?: throw IOException("Failed to create $target")
context.contentResolver.openOutputStream(targetDocFile.uri)?.use { os ->
context.contentResolver.openInputStream(sourceDocFile.uri)?.use { is_ ->
is_.copyTo(os)
}
}
}
override fun move(source: Path, target: Path, vararg options: CopyOption) {
TODO("Not yet implemented")
check(source is SafPath)
check(target is SafPath)
val sourceUri = source.getDocumentFile(context)!!.uri
val parentUri = source.parent.getDocumentFile(context)!!.uri
val destParentUri = target.parent.getDocumentFile(context)!!.uri
// 1. If dest parent is the same as source parent, rename the file
if (parentUri == destParentUri) {
try {
val newUri = DocumentsContract.renameDocument(
context.contentResolver,
sourceUri,
target.names.last()
)
source.safUri = newUri!!
return
} catch (ignored: FileNotFoundException) {
// no-op: fallback to the next method
}
}
override fun isSameFile(path: Path, path2: Path): Boolean {
TODO("Not yet implemented")
val sourceTreeDocumentId = DocumentsContract.getTreeDocumentId(parentUri)
val destTreeDocumentId = DocumentsContract.getTreeDocumentId(destParentUri)
// 2. If source and dest are in the same tree, and the API level is high enough, move the file
if (sourceTreeDocumentId == destTreeDocumentId &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
) {
val newUri = DocumentsContract.moveDocument(
context.contentResolver,
sourceUri,
parentUri,
destParentUri
)
source.safUri = newUri!!
return
}
// 3. Else copy and delete the file
copy(source, target, *options)
delete(source)
}
override fun isSameFile(p1: Path, p2: Path): Boolean {
check(p1 is SafPath)
check(p2 is SafPath)
return p1.root == p2.root && p1.names == p2.names &&
p1.getDocumentFile(context)!!.uri == p2.getDocumentFile(context)!!.uri
}
override fun isHidden(path: Path): Boolean {
TODO("Not yet implemented")
check(path is SafPath)
if (path.isRoot()) {
return false
}
override fun getFileStore(path: Path): FileStore {
TODO("Not yet implemented")
return path.names.last().startsWith(".")
}
override fun getFileStore(path: Path): FileStore? {
// SAF does not support file store
Log.i(TAG, "getFileStore($path) not implemented")
return null
}
override fun checkAccess(path: Path, vararg modes: AccessMode) {
TODO("Not yet implemented")
check(path is SafPath)
if (path.isRoot()) {
modes.forEach {
when (it) {
AccessMode.READ -> {
// Root is always readable
}
AccessMode.WRITE -> {
// Root is not writable
throw java.nio.file.AccessDeniedException("/") // No kotlin.AccessDeniedException, they are different
}
AccessMode.EXECUTE -> {
// Root is not executable
throw java.nio.file.AccessDeniedException("/") // No kotlin.AccessDeniedException, they are different
}
}
}
return
}
val docFile = path.getDocumentFile(context)
?: throw java.nio.file.NoSuchFileException(
path.toString(),
) // No kotlin.NoSuchFileException, they are different
if (!docFile.exists()) {
throw java.nio.file.NoSuchFileException(
docFile.uri.toString(),
) // No kotlin.NoSuchFileException, they are different
}
modes.forEach {
when (it) {
AccessMode.READ -> {
if (!docFile.canRead()) {
throw java.nio.file.AccessDeniedException(docFile.uri.toString()) // No kotlin.AccessDeniedException, they are different
}
}
AccessMode.WRITE -> {
if (!docFile.canWrite()) {
throw java.nio.file.AccessDeniedException(docFile.uri.toString()) // No kotlin.AccessDeniedException, they are different
}
}
AccessMode.EXECUTE -> {
// SAF files is not executable
throw java.nio.file.AccessDeniedException(docFile.uri.toString()) // No kotlin.AccessDeniedException, they are different
}
}
}
}
override fun <V : FileAttributeView> getFileAttributeView(
path: Path,
type: Class<V>,
vararg options: LinkOption?
): V {
TODO("Not yet implemented")
): V? {
check(path is SafPath)
if (path.isRoot()) {
if (type == BasicFileAttributeView::class.java) {
@Suppress("UNCHECKED_CAST")
return RootBasicFileAttributeView as V
}
if (type == PosixFileAttributeView::class.java) {
@Suppress("UNCHECKED_CAST")
return RootPosixFileAttributeView as V
}
}
if (type == BasicFileAttributeView::class.java) {
@Suppress("UNCHECKED_CAST")
return object : BasicFileAttributeView {
override fun name(): String = "basic"
override fun readAttributes(): BasicFileAttributes =
readAttributes(path, BasicFileAttributes::class.java)
override fun setTimes(
lastModifiedTime: FileTime?,
lastAccessTime: FileTime?,
createTime: FileTime?
) {
Log.w(
TAG,
"setTimes($path, $lastModifiedTime, $lastAccessTime, $createTime) not implemented"
)
}
} as V
}
if (type == PosixFileAttributeView::class.java) {
@Suppress("UNCHECKED_CAST")
return object : PosixFileAttributeView {
override fun name(): String = "posix"
override fun readAttributes(): PosixFileAttributes =
readAttributes(path, PosixFileAttributes::class.java)
override fun setTimes(
lastModifiedTime: FileTime?,
lastAccessTime: FileTime?,
createTime: FileTime?
) {
Log.w(
TAG,
"setTimes($path, $lastModifiedTime, $lastAccessTime, $createTime) not implemented"
)
}
override fun getOwner(): UserPrincipal? {
Log.i(TAG, "getOwner($path) not implemented")
return null
}
override fun setOwner(owner: UserPrincipal?) {
Log.i(TAG, "setOwner($path, $owner) not implemented")
}
override fun setPermissions(perms: MutableSet<PosixFilePermission>?) {
Log.i(TAG, "setPermissions($path, $perms) not implemented")
}
override fun setGroup(group: GroupPrincipal?) {
Log.i(TAG, "setGroup($path, $group) not implemented")
}
} as V
}
Log.w(TAG, "getFileAttributeView($path)[${type.getSimpleName()}] not implemented")
return null
}
override fun <A : BasicFileAttributes> readAttributes(
@@ -90,16 +435,110 @@ class SafFileSystemProvider: FileSystemProvider() {
type: Class<A>,
vararg options: LinkOption?
): A {
// TODO
throw UnsupportedOperationException("readAttributes($path)[${type.getSimpleName()}] N/A");
check(path is SafPath)
if (path.isRoot()) {
if (type == BasicFileAttributes::class.java || type == PosixFileAttributes::class.java) {
@Suppress("UNCHECKED_CAST")
return RootFileAttributes as A
}
}
path.getDocumentFile(context).let {
if (it == null) {
throw java.nio.file.NoSuchFileException(
path.toString(),
) // No kotlin.NoSuchFileException, they are different
}
if (type == BasicFileAttributes::class.java || type == PosixFileAttributes::class.java) {
@Suppress("UNCHECKED_CAST")
return object : PosixFileAttributes {
override fun lastModifiedTime(): FileTime =
FileTime.fromMillis(it.lastModified())
override fun lastAccessTime(): FileTime = FileTime.fromMillis(it.lastModified())
override fun creationTime(): FileTime = FileTime.fromMillis(it.lastModified())
override fun size(): Long = it.length()
override fun fileKey(): Any? = null
override fun isDirectory(): Boolean = it.isDirectory
override fun isRegularFile(): Boolean = it.isFile
override fun isSymbolicLink(): Boolean = false
override fun isOther(): Boolean = false
override fun owner(): UserPrincipal? = null
override fun group(): GroupPrincipal? = null
override fun permissions(): Set<PosixFilePermission> = // 660 for SAF
setOf(
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
)
} as A
}
}
Log.w(TAG, "readAttributes($path)[${type.getSimpleName()}] not implemented")
throw UnsupportedOperationException("readAttributes($path)[${type.getSimpleName()}] N/A")
}
override fun readAttributes(
path: Path,
attributes: String,
vararg options: LinkOption?
): MutableMap<String, Any?> {
TODO("Not yet implemented")
): Map<String, Any?> {
// TODO: Implement readAttributes
check(path is SafPath)
if (path.isRoot()) {
if (attributes == "basic" || attributes.startsWith("basic:")) {
return mapOf(
"isDirectory" to true,
"isRegularFile" to false,
"isSymbolicLink" to false,
"isOther" to false,
"size" to 0L,
"fileKey" to null,
"lastModifiedTime" to FileTime.fromMillis(0),
"lastAccessTime" to FileTime.fromMillis(0),
"creationTime" to FileTime.fromMillis(0)
)
}
if (attributes == "posix" || attributes.startsWith("posix:")) {
return mapOf(
"owner" to null,
"group" to null,
"permissions" to setOf(
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
)
)
}
}
val documentFile = path.getDocumentFile(context)
check(documentFile != null)
if (attributes == "basic" || attributes.startsWith("basic:")) {
return mapOf(
"isDirectory" to documentFile.isDirectory,
"isRegularFile" to documentFile.isFile,
"isSymbolicLink" to false,
"isOther" to false,
"size" to documentFile.length(),
"fileKey" to null,
"lastModifiedTime" to FileTime.fromMillis(documentFile.lastModified()),
"lastAccessTime" to FileTime.fromMillis(documentFile.lastModified()),
"creationTime" to FileTime.fromMillis(documentFile.lastModified())
)
}
if (attributes == "posix" || attributes.startsWith("posix:")) {
return mapOf(
"owner" to null,
"group" to null,
"permissions" to setOf(
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
)
)
}
Log.w(TAG, "readAttributes($path, $attributes) not implemented")
throw UnsupportedOperationException("readAttributes($path, $attributes) N/A")
}
override fun setAttribute(
@@ -108,6 +547,40 @@ class SafFileSystemProvider: FileSystemProvider() {
value: Any?,
vararg options: LinkOption?
) {
TODO("Not yet implemented")
check(path is SafPath)
when (attribute) {
"basic:lastModifiedTime" -> {
check(value is FileTime)
path.getDocumentFile(context)?.let {
val updateValues = ContentValues().apply {
put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, value.toMillis())
}
if (context.contentResolver.update(it.uri, updateValues, null, null) == 0) {
throw IOException("Failed to set lastModifiedTime of $path")
}
} ?: throw java.nio.file.NoSuchFileException(
path.toString(),
) // No kotlin.NoSuchFileException, they are different
}
"basic:lastAccessTime", "basic:creationTime" -> {
check(value is FileTime)
throw UnsupportedOperationException("$attribute is read-only")
}
"posix:owner", "posix:group", "posix:permissions" -> {
Log.w(TAG, "set posix attribute $attribute not implemented")
}
else -> {
Log.w(TAG, "setAttribute($path, $attribute, $value) not implemented")
}
}
// TODO: Implement setAttribute
Log.w(TAG, "setAttribute($path, $attribute, $value) not implemented")
}
companion object {
private const val TAG = "SafFileSystemProvider"
}
}

View File

@@ -1,14 +1,38 @@
/*
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import org.apache.sshd.common.file.util.BasePath
import java.nio.file.LinkOption
import java.nio.file.Path
class SafPath(
fileSystem: SafFileSystem,
root: String, names: List<String>
var safUri: Uri?,
val root: String?, val names: List<String>
) : BasePath<SafPath, SafFileSystem>(fileSystem, root, names) {
override fun toRealPath(vararg options: LinkOption?): Path {
return this // FIXME
override fun toRealPath(vararg options: LinkOption?): SafPath {
return this.normalize()
}
fun getDocumentFile(ctx: Context): DocumentFile? {
if (safUri == null) return null
return DocumentFile.fromTreeUri(ctx, safUri!!)
}
fun isRoot(): Boolean {
return (root == "/") && names.isEmpty()
}
companion object {
fun newRootPath(fileSystem: SafFileSystem): SafPath {
return SafPath(fileSystem, null, "/", emptyList())
}
}
}