mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-31 06:05:12 +00:00
feat(sftp): notify MediaStore on file changes
This commit adds functionality to notify the MediaStore when files are modified via SFTP. This ensures that changes made through SFTP are reflected in the Android media library. Specifically, the MediaStore is notified after file creation, deletion, copying, renaming, and link creation. Additionally, it is notified after closing a file that was opened for writing. This ensures that the MediaStore is kept up-to-date with any changes made to files through SFTP.
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
package org.kde.kdeconnect.Plugins.SftpPlugin
|
package org.kde.kdeconnect.Plugins.SftpPlugin
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory
|
import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory
|
||||||
@@ -26,6 +27,7 @@ import org.apache.sshd.sftp.server.SftpFileSystemAccessor
|
|||||||
import org.apache.sshd.sftp.server.SftpSubsystemFactory
|
import org.apache.sshd.sftp.server.SftpSubsystemFactory
|
||||||
import org.apache.sshd.sftp.server.SftpSubsystemProxy
|
import org.apache.sshd.sftp.server.SftpSubsystemProxy
|
||||||
import org.kde.kdeconnect.Device
|
import org.kde.kdeconnect.Device
|
||||||
|
import org.kde.kdeconnect.Helpers.MediaStoreHelper
|
||||||
import org.kde.kdeconnect.Helpers.RandomHelper
|
import org.kde.kdeconnect.Helpers.RandomHelper
|
||||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper
|
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper
|
||||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.constantTimeCompare
|
import org.kde.kdeconnect.Helpers.SecurityHelpers.constantTimeCompare
|
||||||
@@ -33,10 +35,13 @@ import org.kde.kdeconnect.Plugins.SftpPlugin.saf.SafFileSystemFactory
|
|||||||
import org.kde.kdeconnect.Plugins.SftpPlugin.saf.SafPath
|
import org.kde.kdeconnect.Plugins.SftpPlugin.saf.SafPath
|
||||||
import org.slf4j.impl.HandroidLoggerAdapter
|
import org.slf4j.impl.HandroidLoggerAdapter
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.nio.channels.Channel
|
||||||
import java.nio.channels.SeekableByteChannel
|
import java.nio.channels.SeekableByteChannel
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.CopyOption
|
||||||
import java.nio.file.OpenOption
|
import java.nio.file.OpenOption
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
import java.nio.file.attribute.FileAttribute
|
import java.nio.file.attribute.FileAttribute
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
@@ -103,6 +108,17 @@ internal class SimpleSftpServer {
|
|||||||
sshd.subsystemFactories =
|
sshd.subsystemFactories =
|
||||||
listOf<SubsystemFactory>(SftpSubsystemFactory.Builder().apply {
|
listOf<SubsystemFactory>(SftpSubsystemFactory.Builder().apply {
|
||||||
withFileSystemAccessor(object : SftpFileSystemAccessor {
|
withFileSystemAccessor(object : SftpFileSystemAccessor {
|
||||||
|
fun notifyMediaStore(path: Path) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
val uri = Uri.parse(path.toUri().toString())
|
||||||
|
MediaStoreHelper.indexFile(context, uri)
|
||||||
|
uri
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { Log.i(TAG, "Notified media store: $path, $it") },
|
||||||
|
onFailure = { Log.w(TAG, "Failed to notify media store: $path", it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun openFile(
|
override fun openFile(
|
||||||
subsystem: SftpSubsystemProxy?,
|
subsystem: SftpSubsystemProxy?,
|
||||||
fileHandle: FileHandle?,
|
fileHandle: FileHandle?,
|
||||||
@@ -116,6 +132,61 @@ internal class SimpleSftpServer {
|
|||||||
}
|
}
|
||||||
return super.openFile(subsystem, fileHandle, file, handle, options, *attrs)
|
return super.openFile(subsystem, fileHandle, file, handle, options, *attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun removeFile(
|
||||||
|
subsystem: SftpSubsystemProxy?,
|
||||||
|
path: Path?,
|
||||||
|
isDirectory: Boolean
|
||||||
|
) {
|
||||||
|
super.removeFile(subsystem, path, isDirectory)
|
||||||
|
path?.let { notifyMediaStore(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun copyFile(
|
||||||
|
subsystem: SftpSubsystemProxy?,
|
||||||
|
src: Path?,
|
||||||
|
dst: Path?,
|
||||||
|
opts: MutableCollection<CopyOption>?
|
||||||
|
) {
|
||||||
|
super.copyFile(subsystem, src, dst, opts)
|
||||||
|
dst?.let { notifyMediaStore(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renameFile(
|
||||||
|
subsystem: SftpSubsystemProxy?,
|
||||||
|
oldPath: Path?,
|
||||||
|
newPath: Path?,
|
||||||
|
opts: MutableCollection<CopyOption>?
|
||||||
|
) {
|
||||||
|
super.renameFile(subsystem, oldPath, newPath, opts)
|
||||||
|
oldPath?.let { notifyMediaStore(it) }
|
||||||
|
newPath?.let { notifyMediaStore(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLink(
|
||||||
|
subsystem: SftpSubsystemProxy?,
|
||||||
|
link: Path?,
|
||||||
|
existing: Path?,
|
||||||
|
symLink: Boolean
|
||||||
|
) {
|
||||||
|
super.createLink(subsystem, link, existing, symLink)
|
||||||
|
link?.let { notifyMediaStore(it) }
|
||||||
|
existing?.let { notifyMediaStore(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun closeFile(
|
||||||
|
subsystem: SftpSubsystemProxy?,
|
||||||
|
fileHandle: FileHandle?,
|
||||||
|
file: Path?,
|
||||||
|
handle: String?,
|
||||||
|
channel: Channel?,
|
||||||
|
options: MutableSet<out OpenOption>?
|
||||||
|
) {
|
||||||
|
super.closeFile(subsystem, fileHandle, file, handle, channel, options)
|
||||||
|
if (options?.contains(StandardOpenOption.WRITE) == true) {
|
||||||
|
file?.let { notifyMediaStore(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}.build())
|
}.build())
|
||||||
|
|
||||||
@@ -189,8 +260,9 @@ internal class SimpleSftpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val PORT_RANGE = 1739..1764
|
private const val TAG = "SimpleSftpServer"
|
||||||
|
|
||||||
|
private val PORT_RANGE = 1739..1764
|
||||||
const val USER: String = "kdeconnect"
|
const val USER: String = "kdeconnect"
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@@ -98,7 +98,6 @@ class SafFileSystemProvider(
|
|||||||
val docFile = parent.createFile(Files.probeContentType(path), path.names.last())
|
val docFile = parent.createFile(Files.probeContentType(path), path.names.last())
|
||||||
?: throw IOException("Failed to create $path")
|
?: throw IOException("Failed to create $path")
|
||||||
val uri = docFile.uri
|
val uri = docFile.uri
|
||||||
MediaStoreHelper.indexFile(context, uri)
|
|
||||||
path.safUri = uri
|
path.safUri = uri
|
||||||
return uri
|
return uri
|
||||||
}
|
}
|
||||||
@@ -226,7 +225,6 @@ class SafFileSystemProvider(
|
|||||||
if (!docFile.delete()) {
|
if (!docFile.delete()) {
|
||||||
throw IOException("Failed to delete $path")
|
throw IOException("Failed to delete $path")
|
||||||
}
|
}
|
||||||
MediaStoreHelper.indexFile(context, docFile.uri)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun copy(source: Path, target: Path, vararg options: CopyOption) {
|
override fun copy(source: Path, target: Path, vararg options: CopyOption) {
|
||||||
@@ -272,8 +270,6 @@ class SafFileSystemProvider(
|
|||||||
if (newUri == null) { // renameDocument returns null on failure
|
if (newUri == null) { // renameDocument returns null on failure
|
||||||
return@firstStep
|
return@firstStep
|
||||||
}
|
}
|
||||||
MediaStoreHelper.indexFile(context, sourceUri)
|
|
||||||
MediaStoreHelper.indexFile(context, newUri)
|
|
||||||
source.safUri = newUri
|
source.safUri = newUri
|
||||||
return
|
return
|
||||||
} catch (ignored: FileNotFoundException) {
|
} catch (ignored: FileNotFoundException) {
|
||||||
@@ -295,8 +291,6 @@ class SafFileSystemProvider(
|
|||||||
parentUri,
|
parentUri,
|
||||||
destParentUri
|
destParentUri
|
||||||
)
|
)
|
||||||
MediaStoreHelper.indexFile(context, sourceUri)
|
|
||||||
MediaStoreHelper.indexFile(context, newUri)
|
|
||||||
source.safUri = newUri!!
|
source.safUri = newUri!!
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -7,9 +7,9 @@ package org.kde.kdeconnect.Plugins.SftpPlugin.saf
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import org.apache.sshd.common.file.util.BasePath
|
import org.apache.sshd.common.file.util.BasePath
|
||||||
|
import java.net.URI
|
||||||
import java.nio.file.LinkOption
|
import java.nio.file.LinkOption
|
||||||
|
|
||||||
class SafPath(
|
class SafPath(
|
||||||
@@ -21,6 +21,10 @@ class SafPath(
|
|||||||
return this.normalize()
|
return this.normalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toUri(): URI {
|
||||||
|
return URI.create(safUri.toString()) ?: throw IllegalStateException("SafUri is null")
|
||||||
|
}
|
||||||
|
|
||||||
fun getDocumentFile(ctx: Context): DocumentFile? {
|
fun getDocumentFile(ctx: Context): DocumentFile? {
|
||||||
if (safUri == null) return null
|
if (safUri == null) return null
|
||||||
return DocumentFile.fromTreeUri(ctx, safUri!!)
|
return DocumentFile.fromTreeUri(ctx, safUri!!)
|
||||||
|
Reference in New Issue
Block a user