2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-31 14:15:14 +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:
ShellWen Chen
2024-08-05 18:44:49 +08:00
parent 167e2c7176
commit df0f2d651c
3 changed files with 78 additions and 8 deletions

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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!!)