diff --git a/build.gradle.kts b/build.gradle.kts index 9a5bac59..c9f8dc23 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -132,7 +132,7 @@ android { } dependencies { - coreLibraryDesugaring(libs.android.desugarJdkLibs) + coreLibraryDesugaring(libs.android.desugarJdkLibsNio) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.ui.tooling.preview) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b5de350..5b603cc6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,13 +32,13 @@ reactiveStreams = "1.0.4" recyclerview = "1.3.2" rxjava = "2.2.21" sl4j = "2.0.4" -sshdCore = "0.14.0" +sshdCore = "1.0.0" swiperefreshlayout = "1.1.0" uiToolingPreview = "1.6.7" univocityParsers = "2.9.1" [libraries] -android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } +android-desugarJdkLibsNio = { module = "com.android.tools:desugar_jdk_libs_nio", version.ref = "androidDesugarJdkLibs" } android-smsmms = { module = "org.kde.invent.sredman:android-smsmms", version.ref = "androidSmsmms" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidFileSystemFactory.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidFileSystemFactory.java deleted file mode 100644 index b8d9a12e..00000000 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidFileSystemFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2018 Erik Duisters - * - * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - */ - -package org.kde.kdeconnect.Plugins.SftpPlugin; - -import android.content.Context; - -import org.apache.sshd.common.Session; -import org.apache.sshd.common.file.FileSystemFactory; -import org.apache.sshd.common.file.FileSystemView; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -class AndroidFileSystemFactory implements FileSystemFactory { - final private Context context; - final Map roots; - - AndroidFileSystemFactory(Context context) { - this.context = context; - this.roots = new HashMap<>(); - } - - void initRoots(List storageInfoList) { - for (SftpPlugin.StorageInfo curStorageInfo : storageInfoList) { - if (curStorageInfo.isFileUri()) { - if (curStorageInfo.uri.getPath() != null){ - roots.put(curStorageInfo.displayName, curStorageInfo.uri.getPath()); - } - } else if (curStorageInfo.isContentUri()){ - roots.put(curStorageInfo.displayName, curStorageInfo.uri.toString()); - } - } - } - - @Override - public FileSystemView createFileSystemView(final Session username) { - return new AndroidSafFileSystemView(roots, username.getUsername(), context); - } -} diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidFileSystemView.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidFileSystemView.java deleted file mode 100644 index 150255a0..00000000 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidFileSystemView.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2018 Erik Duisters - * - * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - */ - -package org.kde.kdeconnect.Plugins.SftpPlugin; - -import android.content.Context; - -import org.apache.sshd.common.file.FileSystemView; -import org.apache.sshd.common.file.SshFile; -import org.apache.sshd.common.file.nativefs.NativeFileSystemView; -import org.apache.sshd.common.file.nativefs.NativeSshFile; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -class AndroidFileSystemView extends NativeFileSystemView { - final private String userName; - final private Context context; - private final Map roots; - private final RootFile rootFile; - - AndroidFileSystemView(Map roots, String currentRoot, final String userName, Context context) { - super(userName, roots, currentRoot, File.separatorChar, true); - this.roots = roots; - this.userName = userName; - this.context = context; - this.rootFile = new RootFile( createFileList(), userName, true); - } - - private List createFileList() { - List list = new ArrayList<>(); - for (Map.Entry entry : roots.entrySet()) { - String displayName = entry.getKey(); - String path = entry.getValue(); - - list.add(createNativeSshFile(displayName, new File(path), userName)); - } - - return list; - } - - @Override - public SshFile getFile(String file) { - return getFile("/", file); - } - - @Override - public SshFile getFile(SshFile baseDir, String file) { - return getFile(baseDir.getAbsolutePath(), file); - } - - @Override - protected SshFile getFile(String dir, String file) { - if (!dir.endsWith("/")) { - dir = dir + "/"; - } - - if (!file.startsWith("/")) { - file = dir + file; - } - - String filename = NativeSshFile.getPhysicalName("/", "/", file, false); - - if (filename.equals("/")) { - return rootFile; - } - - for (String root : roots.keySet()) { - if (filename.indexOf(root) == 1) { - String nameWithoutRoot = filename.substring(root.length() + 1); - String path = roots.get(root); - - if (nameWithoutRoot.isEmpty()) { - return createNativeSshFile(filename, new File(path), userName); - } else { - return createNativeSshFile(filename, new File(path, nameWithoutRoot), userName); - } - } - } - - //It's a file under / but not one covered by any Tree - return new RootFile(new ArrayList<>(0), userName, false); - } - - // NativeFileSystemView.getFile(), NativeSshFile.getParentFile() and NativeSshFile.listSshFiles() call - // createNativeSshFile to create new NativeSshFiles so override that instead of getFile() to always create an AndroidSshFile - @Override - public AndroidSshFile createNativeSshFile(String name, File file, String username) { - return new AndroidSshFile(this, name, file, username, context); - } - - @Override - public FileSystemView getNormalizedView() { - return this; - } -} diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafFileSystemView.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafFileSystemView.java deleted file mode 100644 index 33f13f9e..00000000 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafFileSystemView.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2018 Erik Duisters - * - * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - */ - -package org.kde.kdeconnect.Plugins.SftpPlugin; - -import android.content.Context; -import android.net.Uri; -import android.provider.DocumentsContract; - -import org.apache.sshd.common.file.FileSystemView; -import org.apache.sshd.common.file.SshFile; -import org.apache.sshd.common.file.nativefs.NativeSshFile; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class AndroidSafFileSystemView implements FileSystemView { - final String userName; - final Context context; - private final Map roots; - private final RootFile rootFile; - - AndroidSafFileSystemView(Map roots, String userName, Context context) { - this.roots = roots; - this.userName = userName; - this.context = context; - this.rootFile = new RootFile( createFileList(), userName, true); - } - - private List createFileList() { - List list = new ArrayList<>(); - for (Map.Entry entry : roots.entrySet()) { - String displayName = entry.getKey(); - String uri = entry.getValue(); - - Uri treeUri = Uri.parse(uri); - Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)); - list.add(createAndroidSafSshFile(null, documentUri, File.separatorChar + displayName)); - } - - return list; - } - - @Override - public SshFile getFile(String file) { - return getFile("/", file); - } - - @Override - public SshFile getFile(SshFile baseDir, String file) { - return getFile(baseDir.getAbsolutePath(), file); - } - - protected SshFile getFile(String dir, String file) { - if (!dir.endsWith("/")) { - dir = dir + "/"; - } - - if (!file.startsWith("/")) { - file = dir + file; - } - - String filename = NativeSshFile.getPhysicalName("/", "/", file, false); - - if (filename.equals("/")) { - return rootFile; - } - - for (String root : roots.keySet()) { - if (filename.indexOf(root) == 1) { - String nameWithoutRoot = filename.substring(root.length() + 1); - String pathOrUri = roots.get(root); - - Uri treeUri = Uri.parse(pathOrUri); - if (nameWithoutRoot.isEmpty()) { - //TreeDocument - Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)); - - return createAndroidSafSshFile(documentUri, documentUri, filename); - } else { - /* - When sharing a root document tree like "Internal Storage" documentUri looks like: - content://com.android.externalstorage.documents/tree/primary:/document/primary: - For a file or folder beneath that the uri looks like: - content://com.android.externalstorage.documents/tree/primary:/document/primary:Folder/file.txt - - Sharing a non root document tree the documentUri looks like: - content://com.android.externalstorage.documents/tree/primary:Download/document/primary:Download - For a file or folder beneath that the uri looks like: - content://com.android.externalstorage.documents/tree/primary:Download/document/primary:Download/Folder/file.txt - */ - String treeDocumentId = DocumentsContract.getTreeDocumentId(treeUri); - File nameWithoutRootFile = new File(nameWithoutRoot); - String parentSuffix = nameWithoutRootFile.getParent(); - String parentDocumentId = treeDocumentId + ("/".equals(parentSuffix) ? "" : parentSuffix); - - Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, parentDocumentId); - - String documentId = treeDocumentId + (treeDocumentId.endsWith(":") ? nameWithoutRoot.substring(1) : nameWithoutRoot); - Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId); - - return createAndroidSafSshFile(parentUri, documentUri, filename); - } - } - } - - //It's a file under / but not one covered by any Tree - return new RootFile(new ArrayList<>(0), userName, false); - } - - public AndroidSafSshFile createAndroidSafSshFile(Uri parentUri, Uri documentUri, String virtualFilename) { - return new AndroidSafSshFile(this, parentUri, documentUri, virtualFilename); - } - - @Override - public FileSystemView getNormalizedView() { - return this; - } -} diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafSshFile.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafSshFile.java deleted file mode 100644 index bd748d1c..00000000 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafSshFile.java +++ /dev/null @@ -1,480 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2018 Erik Duisters - * - * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - */ - -package org.kde.kdeconnect.Plugins.SftpPlugin; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.provider.DocumentsContract; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.apache.sshd.common.file.SshFile; -import org.kde.kdeconnect.Helpers.FilesHelper; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class AndroidSafSshFile implements SshFile { - private static final String TAG = AndroidSafSshFile.class.getSimpleName(); - - private final String virtualFileName; - private DocumentInfo documentInfo; - private Uri parentUri; - private final AndroidSafFileSystemView fileSystemView; - - AndroidSafSshFile(final AndroidSafFileSystemView fileSystemView, Uri parentUri, Uri uri, String virtualFileName) { - this.fileSystemView = fileSystemView; - this.parentUri = parentUri; - this.documentInfo = new DocumentInfo(fileSystemView.context, uri); - this.virtualFileName = virtualFileName; - } - - @Override - public String getAbsolutePath() { - return virtualFileName; - } - - @Override - public String getName() { - /* From NativeSshFile, looks a lot like new File(virtualFileName).getName() to me */ - - // strip the last '/' - String shortName = virtualFileName; - int filelen = virtualFileName.length(); - if (shortName.charAt(filelen - 1) == File.separatorChar) { - shortName = shortName.substring(0, filelen - 1); - } - - // return from the last '/' - int slashIndex = shortName.lastIndexOf(File.separatorChar); - if (slashIndex != -1) { - shortName = shortName.substring(slashIndex + 1); - } - - return shortName; - } - - @Override - public String getOwner() { - return fileSystemView.userName; - } - - @Override - public boolean isDirectory() { - return documentInfo.isDirectory; - } - - @Override - public boolean isFile() { - return documentInfo.isFile; - } - - @Override - public boolean doesExist() { - return documentInfo.exists; - } - - @Override - public long getSize() { - return documentInfo.length; - } - - @Override - public long getLastModified() { - return documentInfo.lastModified; - } - - @Override - public boolean setLastModified(long time) { - //TODO - /* Throws UnsupportedOperationException on API 26 - try { - ContentValues updateValues = new ContentValues(); - updateValues.put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, time); - result = fileSystemView.context.getContentResolver().update(documentInfo.uri, updateValues, null, null) != 0; - documentInfo.lastModified = time; - } catch (NullPointerException ignored) {} - */ - return true; - } - - @Override - public boolean isReadable() { - return documentInfo.canRead; - } - - @Override - public boolean isWritable() { - return documentInfo.canWrite; - } - - @Override - public boolean isExecutable() { - return documentInfo.isDirectory; - } - - @Override - public boolean isRemovable() { - Log.d(TAG, "isRemovable() - is this ever called?"); - - return false; - } - - public SshFile getParentFile() { - Log.d(TAG,"getParentFile() - is this ever called"); - - return null; - } - - @Override - public boolean delete() { - boolean ret; - - try { - ret = DocumentsContract.deleteDocument(fileSystemView.context.getContentResolver(), documentInfo.uri); - } catch (FileNotFoundException e) { - ret = false; - } - - return ret; - } - - @Override - public boolean create() { - return create(parentUri, FilesHelper.getMimeTypeFromFile(virtualFileName), getName()); - } - - private boolean create(Uri parentUri, String mimeType, String name) { - Uri uri = null; - try { - uri = DocumentsContract.createDocument(fileSystemView.context.getContentResolver(), parentUri, mimeType, name); - - if (uri != null) { - documentInfo = new DocumentInfo(fileSystemView.context, uri); - if (!name.equals(documentInfo.displayName)) { - delete(); - return false; - } - } - } catch (FileNotFoundException ignored) {} - - return uri != null; - } - - @Override - public void truncate() { - if (documentInfo.length > 0) { - delete(); - create(); - } - } - - @Override - public boolean move(final SshFile dest) { - boolean success = false; - - Uri destParentUri = ((AndroidSafSshFile)dest).parentUri; - - if (destParentUri.equals(parentUri)) { - //Rename - try { - Uri newUri = DocumentsContract.renameDocument(fileSystemView.context.getContentResolver(), documentInfo.uri, dest.getName()); - if (newUri != null) { - success = true; - documentInfo.uri = newUri; - } - } catch (FileNotFoundException ignored) {} - } else { - // Move: - String sourceTreeDocumentId = DocumentsContract.getTreeDocumentId(parentUri); - String destTreeDocumentId = DocumentsContract.getTreeDocumentId(((AndroidSafSshFile) dest).parentUri); - - if (sourceTreeDocumentId.equals(destTreeDocumentId) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - try { - Uri newUri = DocumentsContract.moveDocument(fileSystemView.context.getContentResolver(), documentInfo.uri, parentUri, destParentUri); - if (newUri != null) { - success = true; - parentUri = destParentUri; - documentInfo.uri = newUri; - } - } catch (Exception e) { - Log.e(TAG,"DocumentsContract.moveDocument() threw an exception", e); - } - } else { - try { - if (dest.create()) { - try (InputStream in = createInputStream(0); OutputStream out = dest.createOutputStream(0)) { - byte[] buffer = new byte[10 * 1024]; - int read; - - while ((read = in.read(buffer)) > 0) { - out.write(buffer, 0, read); - } - - out.flush(); - - delete(); - success = true; - } catch (IOException e) { - if (dest.doesExist()) { - dest.delete(); - } - } - } - } catch (IOException ignored) {} - } - } - - return success; - } - - @Override - public boolean mkdir() { - return create(parentUri, DocumentsContract.Document.MIME_TYPE_DIR, getName()); - } - - @Override - public List listSshFiles() { - if (!documentInfo.isDirectory) { - return null; - } - - final ContentResolver resolver = fileSystemView.context.getContentResolver(); - final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(documentInfo.uri, DocumentsContract.getDocumentId(documentInfo.uri)); - final ArrayList results = new ArrayList<>(); - - Cursor c = resolver.query(childrenUri, new String[] - { DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME }, null, null, null); - - while (c != null && c.moveToNext()) { - final String documentId = c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)); - final String displayName = c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)); - final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(documentInfo.uri, documentId); - results.add(new AndroidSafSshFile(fileSystemView, parentUri, documentUri, virtualFileName + File.separatorChar + displayName)); - } - - if (c != null) { - c.close(); - } - - return Collections.unmodifiableList(results); - } - - @Override - public OutputStream createOutputStream(final long offset) throws IOException { - if (offset != 0) { - throw new IOException("Seeking is not supported."); - } - return fileSystemView.context.getContentResolver().openOutputStream(documentInfo.uri); - } - - @Override - public InputStream createInputStream(final long offset) throws IOException { - InputStream s = fileSystemView.context.getContentResolver().openInputStream(documentInfo.uri); - final long sought = s.skip(offset); - if (sought != offset) { - throw new IOException(String.format("Unable to seek %d bytes, sought %d bytes.", offset, sought)); - } - return s; - } - - @Override - public void handleClose() { - // Nop - } - - @Override - public Map getAttributes(boolean followLinks) { - Map attributes = new HashMap<>(); - for (SshFile.Attribute attr : SshFile.Attribute.values()) { - switch (attr) { - case Uid: - case Gid: - case NLink: - continue; - } - attributes.put(attr, getAttribute(attr, followLinks)); - } - - return attributes; - } - - @Override - public Object getAttribute(Attribute attribute, boolean followLinks) { - Object ret; - - switch (attribute) { - case Size: - ret = documentInfo.length; - break; - case Uid: - case Gid: - ret = 1; - break; - case Owner: - case Group: - ret = getOwner(); - break; - case IsDirectory: - ret = documentInfo.isDirectory; - break; - case IsRegularFile: - ret = documentInfo.isFile; - break; - case IsSymbolicLink: - ret = false; - break; - case Permissions: - Set tmp = new HashSet<>(); - if (documentInfo.canRead) { - tmp.add(SshFile.Permission.UserRead); - tmp.add(SshFile.Permission.GroupRead); - tmp.add(SshFile.Permission.OthersRead); - } - if (documentInfo.canWrite) { - tmp.add(SshFile.Permission.UserWrite); - tmp.add(SshFile.Permission.GroupWrite); - tmp.add(SshFile.Permission.OthersWrite); - } - if (isExecutable()) { - tmp.add(SshFile.Permission.UserExecute); - tmp.add(SshFile.Permission.GroupExecute); - tmp.add(SshFile.Permission.OthersExecute); - } - ret = tmp.isEmpty() - ? EnumSet.noneOf(SshFile.Permission.class) - : EnumSet.copyOf(tmp); - break; - case CreationTime: - case LastModifiedTime: - case LastAccessTime: - ret = documentInfo.lastModified; - break; - case NLink: - ret = 0; - break; - default: - ret = null; - break; - } - - return ret; - } - - @Override - public void setAttributes(Map attributes) { - //TODO: Using Java 7 NIO it should be possible to implement setting a number of attributes but does SaF allow that? - } - - @Override - public void setAttribute(Attribute attribute, Object value) {} - - @Override - public String readSymbolicLink() throws IOException { - throw new IOException("Not Implemented"); - } - - @Override - public void createSymbolicLink(SshFile destination) throws IOException { - throw new IOException("Not Implemented"); - } - - /** - * Retrieve all file info using 1 query to speed things up - * The only fields guaranteed to be initialized are uri and exists - */ - private static class DocumentInfo { - private Uri uri; - private boolean exists; - private boolean canRead; - private boolean canWrite; - private boolean isDirectory; - private boolean isFile; - private long lastModified; - private long length; - @Nullable - private String displayName; - - private static final String[] columns; - - static { - columns = new String[]{ - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_MIME_TYPE, - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_LAST_MODIFIED, - //DocumentsContract.Document.COLUMN_ICON, - DocumentsContract.Document.COLUMN_FLAGS, - DocumentsContract.Document.COLUMN_SIZE - }; - } - - /* - Based on https://github.com/rcketscientist/DocumentActivity - Extracted from android.support.v4.provider.DocumentsContractAPI19 and android.support.v4.provider.DocumentsContractAPI21 - */ - private DocumentInfo(Context c, Uri uri) - { - this.uri = uri; - - try (Cursor cursor = c.getContentResolver().query(uri, columns, null, null, null)) { - exists = cursor != null && cursor.getCount() > 0; - - if (!exists) - return; - - cursor.moveToFirst(); - - //String documentId = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)); - - final boolean readPerm = c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) - == PackageManager.PERMISSION_GRANTED; - final boolean writePerm = c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - == PackageManager.PERMISSION_GRANTED; - - final int flags = cursor.getInt(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS)); - final boolean supportsDelete = (flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0; - final boolean supportsCreate = (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0; - final boolean supportsWrite = (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0; - String mimeType = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)); - final boolean hasMime = !TextUtils.isEmpty(mimeType); - - isDirectory = DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType); - isFile = !isDirectory && hasMime; - - canRead = readPerm && hasMime; - canWrite = writePerm && (supportsDelete || (isDirectory && supportsCreate) || (hasMime && supportsWrite)); - - displayName = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)); - lastModified = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)); - length = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)); - } catch (IllegalArgumentException e) { - //File does not exist, it's probably going to be created - exists = false; - canWrite = true; - } - } - } -} diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSshFile.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSshFile.kt deleted file mode 100644 index 4d0192d0..00000000 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSshFile.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2018 Erik Duisters - * - * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - */ -package org.kde.kdeconnect.Plugins.SftpPlugin - -import android.content.Context -import android.net.Uri -import android.util.Log -import org.apache.commons.io.FileUtils -import org.apache.sshd.common.file.nativefs.NativeSshFile -import org.kde.kdeconnect.Helpers.MediaStoreHelper -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.OutputStream -import java.io.RandomAccessFile - -internal class AndroidSshFile( - view: AndroidFileSystemView, - name: String, - file: File, - userName: String, - private val context: Context -) : NativeSshFile(view, name, file, userName) { - @Throws(IOException::class) - override fun createOutputStream(offset: Long): OutputStream { - if (!isWritable) { - throw IOException("No write permission : ${file.name}") - } - - val raf = RandomAccessFile(file, "rw") - try { - if (offset < raf.length()) { - throw IOException("Your SSHFS is bugged") // SSHFS 3.0 and 3.2 cause data corruption, abort the transfer if this happens - } - raf.setLength(offset) - raf.seek(offset) - - return object : FileOutputStream(raf.fd) { - @Throws(IOException::class) - override fun close() { - super.close() - raf.close() - } - } - } catch (e: IOException) { - raf.close() - throw e - } - } - - override fun delete(): Boolean { - return super.delete().also { - if (it) { - MediaStoreHelper.indexFile(context, Uri.fromFile(file)) - } - } - } - - @Throws(IOException::class) - override fun create(): Boolean { - return super.create().also { - if (it) { - MediaStoreHelper.indexFile(context, Uri.fromFile(file)) - } - } - } - - // Based on https://github.com/wolpi/prim-ftpd/blob/master/primitiveFTPd/src/org/primftpd/filesystem/FsFile.java - override fun doesExist(): Boolean { - // file.exists() returns false when we don't have read permission - // try to figure out if it really does not exist - try { - return file.exists() || FileUtils.directoryContains(file.parentFile, file) - } catch (e: IOException) { - // An IllegalArgumentException is thrown if the parent is null or not a directory. - Log.d(TAG, "Exception: ", e) - } catch (e: IllegalArgumentException) { - Log.d(TAG, "Exception: ", e) - } - - return false - } - - companion object { - private val TAG: String = AndroidSshFile::class.java.simpleName - } -} diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/DHG14_256.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/DHG14_256.kt deleted file mode 100644 index 39002155..00000000 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/DHG14_256.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora - * - * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - */ -package org.kde.kdeconnect.Plugins.SftpPlugin - -import org.apache.sshd.common.KeyExchange -import org.apache.sshd.common.NamedFactory -import org.apache.sshd.common.digest.SHA256 -import org.apache.sshd.common.kex.AbstractDH -import org.apache.sshd.common.kex.DH -import org.apache.sshd.common.kex.DHGroupData -import org.apache.sshd.server.kex.AbstractDHGServer - -class DHG14_256 : AbstractDHGServer() { - class Factory : NamedFactory { - override fun getName(): String = "diffie-hellman-group14-sha256" - - override fun create(): KeyExchange { - return DHG14_256() - } - } - - @Throws(Exception::class) - override fun getDH(): AbstractDH { - return DH(SHA256.Factory()).apply { - setG(DHGroupData.getG()) - setP(DHGroupData.getP14()) - } - } -} diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/DHG14_256Factory.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/DHG14_256Factory.kt new file mode 100644 index 00000000..8f121b29 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/DHG14_256Factory.kt @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2024 ShellWen Chen + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.Plugins.SftpPlugin + +import org.apache.sshd.common.digest.BuiltinDigests +import org.apache.sshd.common.kex.AbstractDH +import org.apache.sshd.common.kex.DHFactory +import org.apache.sshd.common.kex.DHG +import org.apache.sshd.common.kex.DHGroupData +import org.apache.sshd.common.util.SecurityUtils +import java.math.BigInteger + +object DHG14_256Factory : DHFactory { + override fun getName(): String = "diffie-hellman-group14-sha256" + + override fun isSupported(): Boolean = SecurityUtils.isBouncyCastleRegistered() + + override fun isGroupExchange(): Boolean = false + + override fun create(vararg params: Any?): AbstractDH { + require(params.isEmpty()) { "No accepted parameters for $name" } + return DHG( + BuiltinDigests.sha256, + BigInteger(DHGroupData.getP14()), + BigInteger(DHGroupData.getG()) + ) + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.kt deleted file mode 100644 index ea729214..00000000 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2018 Erik Duisters - * - * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - */ -package org.kde.kdeconnect.Plugins.SftpPlugin - -import org.apache.sshd.common.file.SshFile -import java.io.InputStream -import java.io.OutputStream -import java.util.Calendar -import java.util.Collections -import java.util.EnumMap -import java.util.EnumSet - -// TODO: ls .. and ls / only show .. and / respectively I would expect a listing -// TODO: cd .. to / does not work and prints "Can't change directory: Can't check target" -internal class RootFile( - private val files: List, - private val userName: String, - private val exists: Boolean -) : SshFile { - override fun getAbsolutePath(): String = "/" - - override fun getName(): String = "/" - - override fun getAttributes(followLinks: Boolean): Map { - val attrs: MutableMap = EnumMap(SshFile.Attribute::class.java) - - attrs[SshFile.Attribute.Size] = 0 - attrs[SshFile.Attribute.Owner] = userName - attrs[SshFile.Attribute.Group] = userName - - val p = EnumSet.noneOf( - SshFile.Permission::class.java - ) - p.add(SshFile.Permission.UserExecute) - p.add(SshFile.Permission.GroupExecute) - p.add(SshFile.Permission.OthersExecute) - attrs[SshFile.Attribute.Permissions] = p - - val now = Calendar.getInstance().timeInMillis - attrs[SshFile.Attribute.LastAccessTime] = now - attrs[SshFile.Attribute.LastModifiedTime] = now - - attrs[SshFile.Attribute.IsSymbolicLink] = false - attrs[SshFile.Attribute.IsDirectory] = true - attrs[SshFile.Attribute.IsRegularFile] = false - - return attrs - } - - override fun setAttributes(attributes: Map) {} - - override fun getAttribute(attribute: SshFile.Attribute, followLinks: Boolean): Any? = null - - override fun setAttribute(attribute: SshFile.Attribute, value: Any) {} - - override fun readSymbolicLink(): String = "" - - override fun createSymbolicLink(destination: SshFile) {} - - override fun getOwner(): String? = null - - override fun isDirectory(): Boolean = true - - override fun isFile(): Boolean = false - - override fun doesExist(): Boolean = exists - - override fun isReadable(): Boolean = true - - override fun isWritable(): Boolean = false - - override fun isExecutable(): Boolean = true - - override fun isRemovable(): Boolean = false - - override fun getParentFile(): SshFile = this - - override fun getLastModified(): Long = 0 - - override fun setLastModified(time: Long): Boolean = false - - override fun getSize(): Long = 0 - - override fun mkdir(): Boolean = false - - override fun delete(): Boolean = false - - override fun create(): Boolean = false - - override fun truncate() {} - - override fun move(destination: SshFile): Boolean = false - - override fun listSshFiles(): List = Collections.unmodifiableList(files) - - override fun createOutputStream(offset: Long): OutputStream? = null - - override fun createInputStream(offset: Long): InputStream? = null - - override fun handleClose() { - } -} diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.kt index 9847a1b8..ba230dfe 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.kt +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.kt @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2014 Samoilenko Yuri + * SPDX-FileCopyrightText: 2024 ShellWen Chen * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SignatureRSASHA256.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SignatureRSASHA256.kt index 7fb7119e..82f7237a 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SignatureRSASHA256.kt +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SignatureRSASHA256.kt @@ -1,16 +1,20 @@ /* * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora + * SPDX-FileCopyrightText: 2024 ShellWen Chen * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ package org.kde.kdeconnect.Plugins.SftpPlugin -import org.apache.sshd.common.NamedFactory -import org.apache.sshd.common.Signature import org.apache.sshd.common.signature.AbstractSignature +import org.apache.sshd.common.signature.Signature +import org.apache.sshd.common.signature.SignatureFactory +import org.apache.sshd.common.util.ValidateUtils + class SignatureRSASHA256 : AbstractSignature("SHA256withRSA") { - class Factory : NamedFactory { + object Factory : SignatureFactory { + override fun isSupported(): Boolean = true override fun getName(): String = "rsa-sha2-256" override fun create(): Signature { @@ -25,6 +29,18 @@ class SignatureRSASHA256 : AbstractSignature("SHA256withRSA") { @Throws(Exception::class) override fun verify(sig: ByteArray): Boolean { - return signature.verify(extractSig(sig)) + var data = sig + val encoding = extractEncodedSignature(data) + if (encoding != null) { + val keyType = encoding.first + ValidateUtils.checkTrue( + "rsa-sha2-256" == keyType, + "Mismatched key type: %s", + keyType + ) + data = encoding.second + } + + return signature.verify(data) } } diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.kt index 5ec84962..2eebcb2f 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.kt +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.kt @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2014 Samoilenko Yuri + * SPDX-FileCopyrightText: 2024 ShellWen Chen * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ @@ -8,30 +9,25 @@ package org.kde.kdeconnect.Plugins.SftpPlugin import android.content.Context import android.os.Build import android.util.Log -import org.apache.sshd.SshServer import org.apache.sshd.common.NamedFactory import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory +import org.apache.sshd.common.kex.BuiltinDHFactories import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider -import org.apache.sshd.common.signature.SignatureDSA -import org.apache.sshd.common.signature.SignatureECDSA.NISTP256Factory -import org.apache.sshd.common.signature.SignatureECDSA.NISTP384Factory -import org.apache.sshd.common.signature.SignatureECDSA.NISTP521Factory -import org.apache.sshd.common.signature.SignatureRSA +import org.apache.sshd.common.signature.BuiltinSignatures import org.apache.sshd.common.util.SecurityUtils import org.apache.sshd.server.Command -import org.apache.sshd.server.PasswordAuthenticator -import org.apache.sshd.server.PublickeyAuthenticator +import org.apache.sshd.server.SshServer +import org.apache.sshd.server.auth.password.PasswordAuthenticator +import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator import org.apache.sshd.server.command.ScpCommandFactory -import org.apache.sshd.server.kex.DHG14 -import org.apache.sshd.server.kex.ECDHP256 -import org.apache.sshd.server.kex.ECDHP384 -import org.apache.sshd.server.kex.ECDHP521 +import org.apache.sshd.server.kex.DHGServer import org.apache.sshd.server.session.ServerSession -import org.apache.sshd.server.sftp.SftpSubsystem +import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory import org.kde.kdeconnect.Device import org.kde.kdeconnect.Helpers.RandomHelper import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper import org.kde.kdeconnect.Helpers.SecurityHelpers.constantTimeCompare +import org.kde.kdeconnect.Plugins.SftpPlugin.saf.SafFileSystemFactory import java.io.IOException import java.nio.charset.StandardCharsets import java.security.GeneralSecurityException @@ -53,7 +49,7 @@ internal class SimpleSftpServer { private val sshd: SshServer = SshServer.setUpDefaultServer() - private var safFileSystemFactory: AndroidFileSystemFactory? = null + private var safFileSystemFactory: SafFileSystemFactory? = null fun setSafRoots(storageInfoList: List) { safFileSystemFactory!!.initRoots(storageInfoList) @@ -63,22 +59,25 @@ internal class SimpleSftpServer { fun initialize(context: Context?, device: Device) { sshd.signatureFactories = listOf( - NISTP256Factory(), - NISTP384Factory(), - NISTP521Factory(), - SignatureDSA.Factory(), - SignatureRSASHA256.Factory(), - SignatureRSA.Factory() // Insecure SHA1, left for backwards compatibility + BuiltinSignatures.nistp256, + BuiltinSignatures.nistp384, + BuiltinSignatures.nistp521, + BuiltinSignatures.dsa, + SignatureRSASHA256.Factory, + BuiltinSignatures.rsa // Insecure SHA1, left for backwards compatibility ) sshd.keyExchangeFactories = listOf( - ECDHP256.Factory(), // ecdh-sha2-nistp256 - ECDHP384.Factory(), // ecdh-sha2-nistp384 - ECDHP521.Factory(), // ecdh-sha2-nistp521 - DHG14_256.Factory(), // diffie-hellman-group14-sha256 - DHG14.Factory() // Insecure diffie-hellman-group14-sha1, left for backwards-compatibility. - ) + BuiltinDHFactories.ecdhp256, // ecdh-sha2-nistp256 + BuiltinDHFactories.ecdhp384, // ecdh-sha2-nistp384 + BuiltinDHFactories.ecdhp521, // ecdh-sha2-nistp521 + DHG14_256Factory, // diffie-hellman-group14-sha256 + BuiltinDHFactories.dhg14, // Insecure diffie-hellman-group14-sha1, left for backwards-compatibility. + ).map { + DHGServer.newFactory(it) + } + // Reuse this device keys for the ssh connection as well val keyPair = KeyPair( @@ -92,12 +91,12 @@ internal class SimpleSftpServer { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { sshd.fileSystemFactory = NativeFileSystemFactory() } else { - safFileSystemFactory = AndroidFileSystemFactory(context) - sshd.fileSystemFactory = safFileSystemFactory + safFileSystemFactory = SafFileSystemFactory(context!!) + sshd.fileSystemFactory = safFileSystemFactory // FIXME: This is not working } sshd.commandFactory = ScpCommandFactory() sshd.subsystemFactories = - listOf>(SftpSubsystem.Factory()) + listOf>(SftpSubsystemFactory()) keyAuth.deviceKey = device.certificate.publicKey diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafFileSystem.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafFileSystem.kt new file mode 100644 index 00000000..49fb8521 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafFileSystem.kt @@ -0,0 +1,37 @@ +package org.kde.kdeconnect.Plugins.SftpPlugin.saf + +import android.content.Context +import android.util.Log +import org.apache.sshd.common.file.util.BaseFileSystem +import org.apache.sshd.common.file.util.ImmutableList +import java.nio.file.attribute.UserPrincipalLookupService +import java.nio.file.spi.FileSystemProvider + +class SafFileSystem( + fileSystemProvider: FileSystemProvider, + roots: MutableMap, + username: String, + private val context: Context +) : BaseFileSystem(fileSystemProvider) { + override fun close() { + // no-op + Log.v(TAG, "close") + } + + override fun isOpen(): Boolean = true + + override fun supportedFileAttributeViews(): Set = setOf("basic") + + override fun getUserPrincipalLookupService(): UserPrincipalLookupService { + throw UnsupportedOperationException("SAF does not support user principal lookup") + } + + override fun create(root: String, names: ImmutableList): SafPath { + Log.v(TAG, "create: $root, $names") + return SafPath(this, root, names) + } + + companion object { + private const val TAG = "SafFileSystem" + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafFileSystemFactory.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafFileSystemFactory.kt new file mode 100644 index 00000000..ff8f3522 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafFileSystemFactory.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2018 Erik Duisters + * SPDX-FileCopyrightText: 2024 ShellWen Chen + * + * 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.util.Log +import org.apache.sshd.common.session.Session +import org.apache.sshd.common.file.FileSystemFactory +import org.kde.kdeconnect.Plugins.SftpPlugin.SftpPlugin +import java.nio.file.FileSystem + +class SafFileSystemFactory(private val context: Context) : FileSystemFactory { + private val provider = SafFileSystemProvider() + private val roots: MutableMap = HashMap() + + fun initRoots(storageInfoList: List) { + Log.i(TAG, "initRoots: $storageInfoList") + + for (curStorageInfo in storageInfoList) { + when { + curStorageInfo.isFileUri -> { + TODO("File URI is not supported yet") +// if (curStorageInfo.uri.path != null) { +// roots[curStorageInfo.displayName] = curStorageInfo.uri.path +// } + } + curStorageInfo.isContentUri -> { + roots[curStorageInfo.displayName] = curStorageInfo.uri.toString() + } + else -> { + Log.e(TAG, "Unknown storage URI type: $curStorageInfo") + } + } + } + } + + override fun createFileSystem(session: Session): FileSystem { + return SafFileSystem(provider, roots, session.username, context) + } + + companion object { + private const val TAG = "SafFileSystemFactory" + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafFileSystemProvider.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafFileSystemProvider.kt new file mode 100644 index 00000000..624561dd --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafFileSystemProvider.kt @@ -0,0 +1,113 @@ +package org.kde.kdeconnect.Plugins.SftpPlugin.saf + +import java.net.URI +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.LinkOption +import java.nio.file.OpenOption +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.FileAttribute +import java.nio.file.attribute.FileAttributeView +import java.nio.file.spi.FileSystemProvider + +class SafFileSystemProvider: FileSystemProvider() { + override fun getScheme(): String = "saf" + + override fun newFileSystem(uri: URI, env: MutableMap?): FileSystem { + TODO("Not yet implemented") + } + + override fun getFileSystem(uri: URI): FileSystem { + TODO("Not yet implemented") + } + + override fun getPath(uri: URI): Path { + TODO("Not yet implemented") + } + + override fun newByteChannel( + path: Path, + options: MutableSet, + vararg attrs: FileAttribute<*> + ): SeekableByteChannel { + TODO("Not yet implemented") + } + + override fun newDirectoryStream( + dir: Path, + filter: DirectoryStream.Filter + ): DirectoryStream { + TODO("Not yet implemented") + } + + override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) { + TODO("Not yet implemented") + } + + override fun delete(path: Path) { + TODO("Not yet implemented") + } + + override fun copy(source: Path, target: Path, vararg options: CopyOption) { + TODO("Not yet implemented") + } + + override fun move(source: Path, target: Path, vararg options: CopyOption) { + TODO("Not yet implemented") + } + + override fun isSameFile(path: Path, path2: Path): Boolean { + TODO("Not yet implemented") + } + + override fun isHidden(path: Path): Boolean { + TODO("Not yet implemented") + } + + override fun getFileStore(path: Path): FileStore { + TODO("Not yet implemented") + } + + override fun checkAccess(path: Path, vararg modes: AccessMode) { + TODO("Not yet implemented") + } + + override fun getFileAttributeView( + path: Path, + type: Class, + vararg options: LinkOption? + ): V { + TODO("Not yet implemented") + } + + override fun readAttributes( + path: Path, + type: Class, + vararg options: LinkOption? + ): A { + // TODO + throw UnsupportedOperationException("readAttributes($path)[${type.getSimpleName()}] N/A"); + } + + override fun readAttributes( + path: Path, + attributes: String, + vararg options: LinkOption? + ): MutableMap { + TODO("Not yet implemented") + } + + override fun setAttribute( + path: Path, + attribute: String, + value: Any?, + vararg options: LinkOption? + ) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafPath.kt b/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafPath.kt new file mode 100644 index 00000000..d2593af3 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafPath.kt @@ -0,0 +1,15 @@ +package org.kde.kdeconnect.Plugins.SftpPlugin.saf + +import org.apache.sshd.common.file.util.BasePath +import org.apache.sshd.common.file.util.ImmutableList +import java.nio.file.LinkOption +import java.nio.file.Path + +class SafPath( + fileSystem: SafFileSystem, + root: String, names: ImmutableList +) : BasePath(fileSystem, root, names) { + override fun toRealPath(vararg options: LinkOption?): Path { + return this // FIXME + } +} \ No newline at end of file