mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-09-02 23:25:10 +00:00
refactor: migrate SSHD Core to 1.0.0
. SAF is unavailable now.
This commit is contained in:
committed by
Albert Vaca Cintora
parent
6783f0a167
commit
e37a519e3a
@@ -132,7 +132,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring(libs.android.desugarJdkLibs)
|
coreLibraryDesugaring(libs.android.desugarJdkLibsNio)
|
||||||
|
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
@@ -32,13 +32,13 @@ reactiveStreams = "1.0.4"
|
|||||||
recyclerview = "1.3.2"
|
recyclerview = "1.3.2"
|
||||||
rxjava = "2.2.21"
|
rxjava = "2.2.21"
|
||||||
sl4j = "2.0.4"
|
sl4j = "2.0.4"
|
||||||
sshdCore = "0.14.0"
|
sshdCore = "1.0.0"
|
||||||
swiperefreshlayout = "1.1.0"
|
swiperefreshlayout = "1.1.0"
|
||||||
uiToolingPreview = "1.6.7"
|
uiToolingPreview = "1.6.7"
|
||||||
univocityParsers = "2.9.1"
|
univocityParsers = "2.9.1"
|
||||||
|
|
||||||
[libraries]
|
[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" }
|
android-smsmms = { module = "org.kde.invent.sredman:android-smsmms", version.ref = "androidSmsmms" }
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
||||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
|
||||||
|
@@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
|
||||||
*
|
|
||||||
* 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<String, String> roots;
|
|
||||||
|
|
||||||
AndroidFileSystemFactory(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
this.roots = new HashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void initRoots(List<SftpPlugin.StorageInfo> 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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
|
||||||
*
|
|
||||||
* 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<String, String> roots;
|
|
||||||
private final RootFile rootFile;
|
|
||||||
|
|
||||||
AndroidFileSystemView(Map<String, String> 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<SshFile> createFileList() {
|
|
||||||
List<SshFile> list = new ArrayList<>();
|
|
||||||
for (Map.Entry<String, String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,124 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
|
||||||
*
|
|
||||||
* 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<String, String> roots;
|
|
||||||
private final RootFile rootFile;
|
|
||||||
|
|
||||||
AndroidSafFileSystemView(Map<String, String> roots, String userName, Context context) {
|
|
||||||
this.roots = roots;
|
|
||||||
this.userName = userName;
|
|
||||||
this.context = context;
|
|
||||||
this.rootFile = new RootFile( createFileList(), userName, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<SshFile> createFileList() {
|
|
||||||
List<SshFile> list = new ArrayList<>();
|
|
||||||
for (Map.Entry<String, String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,480 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
|
||||||
*
|
|
||||||
* 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<SshFile> 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<AndroidSafSshFile> 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<Attribute, Object> getAttributes(boolean followLinks) {
|
|
||||||
Map<SshFile.Attribute, Object> 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<Permission> 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<Attribute, Object> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,90 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
|
|
||||||
*
|
|
||||||
* 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<KeyExchange> {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,105 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
|
||||||
*
|
|
||||||
* 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<SshFile>,
|
|
||||||
private val userName: String,
|
|
||||||
private val exists: Boolean
|
|
||||||
) : SshFile {
|
|
||||||
override fun getAbsolutePath(): String = "/"
|
|
||||||
|
|
||||||
override fun getName(): String = "/"
|
|
||||||
|
|
||||||
override fun getAttributes(followLinks: Boolean): Map<SshFile.Attribute, Any> {
|
|
||||||
val attrs: MutableMap<SshFile.Attribute, Any> = 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<SshFile.Attribute, Any>) {}
|
|
||||||
|
|
||||||
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<SshFile> = Collections.unmodifiableList(files)
|
|
||||||
|
|
||||||
override fun createOutputStream(offset: Long): OutputStream? = null
|
|
||||||
|
|
||||||
override fun createInputStream(offset: Long): InputStream? = null
|
|
||||||
|
|
||||||
override fun handleClose() {
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2014 Samoilenko Yuri <kinnalru@gmail.com>
|
* SPDX-FileCopyrightText: 2014 Samoilenko Yuri <kinnalru@gmail.com>
|
||||||
|
* 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
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
*/
|
*/
|
||||||
|
@@ -1,16 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
|
* SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
|
||||||
|
* 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
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
*/
|
*/
|
||||||
package org.kde.kdeconnect.Plugins.SftpPlugin
|
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.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 SignatureRSASHA256 : AbstractSignature("SHA256withRSA") {
|
||||||
class Factory : NamedFactory<Signature> {
|
object Factory : SignatureFactory {
|
||||||
|
override fun isSupported(): Boolean = true
|
||||||
override fun getName(): String = "rsa-sha2-256"
|
override fun getName(): String = "rsa-sha2-256"
|
||||||
|
|
||||||
override fun create(): Signature {
|
override fun create(): Signature {
|
||||||
@@ -25,6 +29,18 @@ class SignatureRSASHA256 : AbstractSignature("SHA256withRSA") {
|
|||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun verify(sig: ByteArray): Boolean {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2014 Samoilenko Yuri <kinnalru@gmail.com>
|
* SPDX-FileCopyrightText: 2014 Samoilenko Yuri <kinnalru@gmail.com>
|
||||||
|
* 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
|
* 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.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.apache.sshd.SshServer
|
|
||||||
import org.apache.sshd.common.NamedFactory
|
import org.apache.sshd.common.NamedFactory
|
||||||
import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory
|
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.keyprovider.AbstractKeyPairProvider
|
||||||
import org.apache.sshd.common.signature.SignatureDSA
|
import org.apache.sshd.common.signature.BuiltinSignatures
|
||||||
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.util.SecurityUtils
|
import org.apache.sshd.common.util.SecurityUtils
|
||||||
import org.apache.sshd.server.Command
|
import org.apache.sshd.server.Command
|
||||||
import org.apache.sshd.server.PasswordAuthenticator
|
import org.apache.sshd.server.SshServer
|
||||||
import org.apache.sshd.server.PublickeyAuthenticator
|
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.command.ScpCommandFactory
|
||||||
import org.apache.sshd.server.kex.DHG14
|
import org.apache.sshd.server.kex.DHGServer
|
||||||
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.session.ServerSession
|
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.Device
|
||||||
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
|
||||||
|
import org.kde.kdeconnect.Plugins.SftpPlugin.saf.SafFileSystemFactory
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
@@ -53,7 +49,7 @@ internal class SimpleSftpServer {
|
|||||||
|
|
||||||
private val sshd: SshServer = SshServer.setUpDefaultServer()
|
private val sshd: SshServer = SshServer.setUpDefaultServer()
|
||||||
|
|
||||||
private var safFileSystemFactory: AndroidFileSystemFactory? = null
|
private var safFileSystemFactory: SafFileSystemFactory? = null
|
||||||
|
|
||||||
fun setSafRoots(storageInfoList: List<SftpPlugin.StorageInfo>) {
|
fun setSafRoots(storageInfoList: List<SftpPlugin.StorageInfo>) {
|
||||||
safFileSystemFactory!!.initRoots(storageInfoList)
|
safFileSystemFactory!!.initRoots(storageInfoList)
|
||||||
@@ -63,22 +59,25 @@ internal class SimpleSftpServer {
|
|||||||
fun initialize(context: Context?, device: Device) {
|
fun initialize(context: Context?, device: Device) {
|
||||||
sshd.signatureFactories =
|
sshd.signatureFactories =
|
||||||
listOf(
|
listOf(
|
||||||
NISTP256Factory(),
|
BuiltinSignatures.nistp256,
|
||||||
NISTP384Factory(),
|
BuiltinSignatures.nistp384,
|
||||||
NISTP521Factory(),
|
BuiltinSignatures.nistp521,
|
||||||
SignatureDSA.Factory(),
|
BuiltinSignatures.dsa,
|
||||||
SignatureRSASHA256.Factory(),
|
SignatureRSASHA256.Factory,
|
||||||
SignatureRSA.Factory() // Insecure SHA1, left for backwards compatibility
|
BuiltinSignatures.rsa // Insecure SHA1, left for backwards compatibility
|
||||||
)
|
)
|
||||||
|
|
||||||
sshd.keyExchangeFactories =
|
sshd.keyExchangeFactories =
|
||||||
listOf(
|
listOf(
|
||||||
ECDHP256.Factory(), // ecdh-sha2-nistp256
|
BuiltinDHFactories.ecdhp256, // ecdh-sha2-nistp256
|
||||||
ECDHP384.Factory(), // ecdh-sha2-nistp384
|
BuiltinDHFactories.ecdhp384, // ecdh-sha2-nistp384
|
||||||
ECDHP521.Factory(), // ecdh-sha2-nistp521
|
BuiltinDHFactories.ecdhp521, // ecdh-sha2-nistp521
|
||||||
DHG14_256.Factory(), // diffie-hellman-group14-sha256
|
DHG14_256Factory, // diffie-hellman-group14-sha256
|
||||||
DHG14.Factory() // Insecure diffie-hellman-group14-sha1, left for backwards-compatibility.
|
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
|
// Reuse this device keys for the ssh connection as well
|
||||||
val keyPair = KeyPair(
|
val keyPair = KeyPair(
|
||||||
@@ -92,12 +91,12 @@ internal class SimpleSftpServer {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
sshd.fileSystemFactory = NativeFileSystemFactory()
|
sshd.fileSystemFactory = NativeFileSystemFactory()
|
||||||
} else {
|
} else {
|
||||||
safFileSystemFactory = AndroidFileSystemFactory(context)
|
safFileSystemFactory = SafFileSystemFactory(context!!)
|
||||||
sshd.fileSystemFactory = safFileSystemFactory
|
sshd.fileSystemFactory = safFileSystemFactory // FIXME: This is not working
|
||||||
}
|
}
|
||||||
sshd.commandFactory = ScpCommandFactory()
|
sshd.commandFactory = ScpCommandFactory()
|
||||||
sshd.subsystemFactories =
|
sshd.subsystemFactories =
|
||||||
listOf<NamedFactory<Command>>(SftpSubsystem.Factory())
|
listOf<NamedFactory<Command>>(SftpSubsystemFactory())
|
||||||
|
|
||||||
keyAuth.deviceKey = device.certificate.publicKey
|
keyAuth.deviceKey = device.certificate.publicKey
|
||||||
|
|
||||||
|
@@ -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<String, String?>,
|
||||||
|
username: String,
|
||||||
|
private val context: Context
|
||||||
|
) : BaseFileSystem<SafPath>(fileSystemProvider) {
|
||||||
|
override fun close() {
|
||||||
|
// no-op
|
||||||
|
Log.v(TAG, "close")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOpen(): Boolean = true
|
||||||
|
|
||||||
|
override fun supportedFileAttributeViews(): Set<String> = setOf("basic")
|
||||||
|
|
||||||
|
override fun getUserPrincipalLookupService(): UserPrincipalLookupService {
|
||||||
|
throw UnsupportedOperationException("SAF does not support user principal lookup")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(root: String, names: ImmutableList<String>): SafPath {
|
||||||
|
Log.v(TAG, "create: $root, $names")
|
||||||
|
return SafPath(this, root, names)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SafFileSystem"
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
||||||
|
* 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.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<String, String?> = HashMap()
|
||||||
|
|
||||||
|
fun initRoots(storageInfoList: List<SftpPlugin.StorageInfo>) {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
@@ -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<String, *>?): 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<out OpenOption>,
|
||||||
|
vararg attrs: FileAttribute<*>
|
||||||
|
): SeekableByteChannel {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newDirectoryStream(
|
||||||
|
dir: Path,
|
||||||
|
filter: DirectoryStream.Filter<in Path>
|
||||||
|
): DirectoryStream<Path> {
|
||||||
|
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 <V : FileAttributeView> getFileAttributeView(
|
||||||
|
path: Path,
|
||||||
|
type: Class<V>,
|
||||||
|
vararg options: LinkOption?
|
||||||
|
): V {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <A : BasicFileAttributes> readAttributes(
|
||||||
|
path: Path,
|
||||||
|
type: Class<A>,
|
||||||
|
vararg options: LinkOption?
|
||||||
|
): A {
|
||||||
|
// TODO
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAttribute(
|
||||||
|
path: Path,
|
||||||
|
attribute: String,
|
||||||
|
value: Any?,
|
||||||
|
vararg options: LinkOption?
|
||||||
|
) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
15
src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafPath.kt
Normal file
15
src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafPath.kt
Normal file
@@ -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<String>
|
||||||
|
) : BasePath<SafPath, SafFileSystem>(fileSystem, root, names) {
|
||||||
|
override fun toRealPath(vararg options: LinkOption?): Path {
|
||||||
|
return this // FIXME
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user