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:
committed by
Albert Vaca Cintora
parent
7fbfc9df90
commit
0fb6e25682
@@ -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")
|
||||
}
|
||||
}
|
@@ -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,
|
||||
)
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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(path: Path, path2: Path): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
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
|
||||
}
|
||||
|
||||
return path.names.last().startsWith(".")
|
||||
}
|
||||
|
||||
override fun getFileStore(path: Path): FileStore {
|
||||
TODO("Not yet implemented")
|
||||
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"
|
||||
}
|
||||
}
|
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user