From f1603c5b61374824da6cfd98aa6a1d9be5fbe6b3 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 27 Mar 2022 14:26:43 -0700 Subject: [PATCH] [Feature] Temporarily remove FTP client implementation for 1.4.1 release. --- app/build.gradle | 1 - app/src/main/AndroidManifest.xml | 5 - .../android/files/app/AppInitializers.kt | 3 - .../files/coil/PathAttributesFetcher.kt | 4 +- .../files/filelist/FileItemExtensions.kt | 4 +- .../files/provider/FileSystemProviders.kt | 6 - .../android/files/provider/ftp/FtpCopyMove.kt | 178 -------- .../provider/ftp/FtpFileAttributeView.kt | 61 --- .../files/provider/ftp/FtpFileAttributes.kt | 46 -- .../files/provider/ftp/FtpFileSystem.kt | 135 ------ .../provider/ftp/FtpFileSystemProvider.kt | 431 ------------------ .../android/files/provider/ftp/FtpPath.kt | 131 ------ .../provider/ftp/IOExceptionFtpExtensions.kt | 16 - .../provider/ftp/OpenOptionsFtpExtensions.kt | 21 - .../files/provider/ftp/PathFtpExtensions.kt | 12 - .../provider/ftp/client/Authenticator.kt | 10 - .../files/provider/ftp/client/Authority.kt | 38 -- .../files/provider/ftp/client/Client.kt | 352 -------------- .../provider/ftp/client/FTPClientCompat.kt | 50 -- .../provider/ftp/client/FileByteChannel.kt | 279 ------------ .../android/files/provider/ftp/client/Mode.kt | 11 - .../ftp/client/NegativeReplyCodeException.kt | 30 -- .../files/provider/ftp/client/Protocol.kt | 22 - .../files/storage/AddStorageDialogFragment.kt | 2 - .../files/storage/EditFtpServerActivity.kt | 28 -- .../files/storage/EditFtpServerFragment.kt | 374 --------------- .../files/storage/EditFtpServerViewModel.kt | 56 --- .../android/files/storage/FtpServer.kt | 51 --- .../files/storage/FtpServerAuthenticator.kt | 36 -- .../res/layout/edit_ftp_server_fragment.xml | 268 ----------- app/src/main/res/raw/licenses.xml | 7 - app/src/main/res/values-zh-rCN/strings.xml | 29 -- app/src/main/res/values-zh-rTW/strings.xml | 29 -- app/src/main/res/values/strings.xml | 35 -- 34 files changed, 2 insertions(+), 2759 deletions(-) delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpCopyMove.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileAttributeView.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileAttributes.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystem.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystemProvider.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpPath.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/IOExceptionFtpExtensions.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/OpenOptionsFtpExtensions.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/PathFtpExtensions.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Authenticator.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Authority.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Client.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/client/FTPClientCompat.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/client/FileByteChannel.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Mode.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/client/NegativeReplyCodeException.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Protocol.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerActivity.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerViewModel.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/storage/FtpServer.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/storage/FtpServerAuthenticator.kt delete mode 100644 app/src/main/res/layout/edit_ftp_server_fragment.xml diff --git a/app/build.gradle b/app/build.gradle index ef76ddc9c..d47b2e885 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,7 +154,6 @@ dependencies { // com.google.guava:listenablefuture:1.0 pulled in by AndroidX Core implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' implementation 'com.takisoft.preferencex:preferencex:1.1.0' - implementation 'commons-net:commons-net:3.8.0' // LicensesDialog 2.2.0 pulls in androidx.webkit and uses setForceDark() instead of correctly // setting colors. //noinspection GradleDependency diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 255f6118d..a63dbc445 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -181,11 +181,6 @@ android:label="@string/storage_edit_document_tree_title" android:theme="@style/Theme.MaterialFiles.Translucent" /> - - - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp - -import java8.nio.file.FileAlreadyExistsException -import java8.nio.file.StandardCopyOption -import me.zhanghai.android.files.compat.toInstantCompat -import me.zhanghai.android.files.provider.common.CopyOptions -import me.zhanghai.android.files.provider.common.copyTo -import me.zhanghai.android.files.provider.ftp.client.Client -import java.io.IOException - -internal object FtpCopyMove { - @Throws(IOException::class) - fun copy(source: FtpPath, target: FtpPath, copyOptions: CopyOptions) { - if (copyOptions.atomicMove) { - throw UnsupportedOperationException(StandardCopyOption.ATOMIC_MOVE.toString()) - } - val sourceFile = try { - Client.listFile(source, copyOptions.noFollowLinks) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(source.toString()) - } - val targetFile = try { - Client.listFileOrNull(target, true) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(target.toString()) - } - val sourceSize = sourceFile.size - if (targetFile != null) { - if (source == target) { - copyOptions.progressListener?.invoke(sourceSize) - return - } - if (!copyOptions.replaceExisting) { - throw FileAlreadyExistsException(source.toString(), target.toString(), null) - } - try { - Client.delete(target, targetFile.isDirectory) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(target.toString()) - } - } - when { - sourceFile.isDirectory -> { - try { - Client.createDirectory(target) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(target.toString()) - } - copyOptions.progressListener?.invoke(sourceSize) - } - sourceFile.isSymbolicLink -> - throw UnsupportedOperationException("Cannot copy symbolic links") - else -> { - val sourceInputStream = try { - Client.retrieveFile(source) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(source.toString()) - } - try { - val targetOutputStream = try { - Client.storeFile(target) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(target.toString()) - } - var successful = false - try { - sourceInputStream.copyTo( - targetOutputStream, copyOptions.progressIntervalMillis, - copyOptions.progressListener - ) - successful = true - } finally { - try { - targetOutputStream.close() - } catch (e: IOException) { - throw IOException(e).toFileSystemExceptionForFtp(target.toString()) - } finally { - if (!successful) { - try { - Client.delete(target, sourceFile.isDirectory) - } catch (e: IOException) { - e.printStackTrace() - } - } - } - } - } finally { - try { - sourceInputStream.close() - } catch (e: IOException) { - throw IOException(e).toFileSystemExceptionForFtp(source.toString()) - } - } - } - } - // We don't take error when copying attribute fatal, so errors will only be logged from now - // on. - if (!sourceFile.isSymbolicLink) { - val timestamp = sourceFile.timestamp - if (timestamp != null) { - try { - Client.setLastModifiedTime(target, sourceFile.timestamp.toInstantCompat()) - } catch (e: IOException) { - e.printStackTrace() - } - } - } - } - - @Throws(IOException::class) - fun move(source: FtpPath, target: FtpPath, copyOptions: CopyOptions) { - val sourceFile = try { - Client.listFile(source, copyOptions.noFollowLinks) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(source.toString()) - } - val targetFile = try { - Client.listFileOrNull(target, true) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(target.toString()) - } - val sourceSize = sourceFile.size - if (targetFile != null) { - if (source == target) { - copyOptions.progressListener?.invoke(sourceSize) - return - } - if (!copyOptions.replaceExisting) { - throw FileAlreadyExistsException(source.toString(), target.toString(), null) - } - try { - Client.delete(target, targetFile.isDirectory) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(target.toString()) - } - } - var renameSuccessful = false - try { - Client.renameFile(source, target) - renameSuccessful = true - } catch (e: IOException) { - if (copyOptions.atomicMove) { - throw e.toFileSystemExceptionForFtp(source.toString(), target.toString()) - } - // Ignored. - } - if (renameSuccessful) { - copyOptions.progressListener?.invoke(sourceSize) - return - } - if (copyOptions.atomicMove) { - throw AssertionError() - } - var copyOptions = copyOptions - if (!copyOptions.copyAttributes || !copyOptions.noFollowLinks) { - copyOptions = CopyOptions( - copyOptions.replaceExisting, true, false, true, copyOptions.progressIntervalMillis, - copyOptions.progressListener - ) - } - copy(source, target, copyOptions) - try { - Client.delete(source, sourceFile.isDirectory) - } catch (e: IOException) { - try { - Client.delete(target, sourceFile.isDirectory) - } catch (e2: IOException) { - e.addSuppressed(e2.toFileSystemExceptionForFtp(target.toString())) - } - throw e.toFileSystemExceptionForFtp(source.toString()) - } - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileAttributeView.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileAttributeView.kt deleted file mode 100644 index 51547f187..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileAttributeView.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp - -import java8.nio.file.LinkOption -import java8.nio.file.attribute.BasicFileAttributeView -import java8.nio.file.attribute.FileTime -import me.zhanghai.android.files.provider.ftp.client.Client -import java.io.IOException - -internal class FtpFileAttributeView( - private val path: FtpPath, - private val noFollowLinks: Boolean -) : BasicFileAttributeView { - override fun name(): String = NAME - - @Throws(IOException::class) - override fun readAttributes(): FtpFileAttributes { - val file = try { - Client.listFile(path, noFollowLinks) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(path.toString()) - } - return FtpFileAttributes.from(file, path) - } - - override fun setTimes( - lastModifiedTime: FileTime?, - lastAccessTime: FileTime?, - createTime: FileTime? - ) { - if (lastModifiedTime == null) { - // Only throw if caller is trying to set only last access time and/or create time, so - // that foreign copy move can still set last modified time. - if (lastAccessTime != null) { - throw UnsupportedOperationException("lastAccessTime") - } - if (createTime != null) { - throw UnsupportedOperationException("createTime") - } - return - } - if (noFollowLinks) { - throw UnsupportedOperationException(LinkOption.NOFOLLOW_LINKS.toString()) - } - try { - Client.setLastModifiedTime(path, lastModifiedTime.toInstant()) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(path.toString()) - } - } - - companion object { - private val NAME = FtpFileSystemProvider.scheme - - val SUPPORTED_NAMES = setOf("basic", NAME) - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileAttributes.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileAttributes.kt deleted file mode 100644 index fce1f253d..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileAttributes.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp - -import android.os.Parcelable -import java8.nio.file.attribute.FileTime -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.WriteWith -import me.zhanghai.android.files.compat.toInstantCompat -import me.zhanghai.android.files.provider.common.AbstractBasicFileAttributes -import me.zhanghai.android.files.provider.common.BasicFileType -import me.zhanghai.android.files.provider.common.FileTimeParceler -import org.apache.commons.net.ftp.FTPFile -import org.threeten.bp.Instant - -@Parcelize -internal data class FtpFileAttributes( - override val lastModifiedTime: @WriteWith FileTime, - override val lastAccessTime: @WriteWith FileTime, - override val creationTime: @WriteWith FileTime, - override val type: BasicFileType, - override val size: Long, - override val fileKey: Parcelable, -) : AbstractBasicFileAttributes() { - companion object { - fun from(file: FTPFile, path: FtpPath): FtpFileAttributes { - val lastModifiedTime = FileTime.from(file.timestamp?.toInstantCompat() ?: Instant.EPOCH) - val lastAccessTime = lastModifiedTime - val creationTime = lastModifiedTime - val type = when { - file.isDirectory -> BasicFileType.DIRECTORY - file.isFile -> BasicFileType.REGULAR_FILE - file.isSymbolicLink -> BasicFileType.SYMBOLIC_LINK - else -> BasicFileType.OTHER - } - val size = file.size.let { if (it != -1L) it else 0 } - val fileKey = path - return FtpFileAttributes( - lastModifiedTime, lastAccessTime, creationTime, type, size, fileKey - ) - } - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystem.kt deleted file mode 100644 index aa57902ea..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystem.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp - -import android.os.Parcel -import android.os.Parcelable -import java8.nio.file.FileStore -import java8.nio.file.FileSystem -import java8.nio.file.Path -import java8.nio.file.PathMatcher -import java8.nio.file.WatchService -import java8.nio.file.attribute.UserPrincipalLookupService -import java8.nio.file.spi.FileSystemProvider -import me.zhanghai.android.files.provider.common.ByteString -import me.zhanghai.android.files.provider.common.ByteStringBuilder -import me.zhanghai.android.files.provider.common.ByteStringListPathCreator -import me.zhanghai.android.files.provider.common.PollingWatchService -import me.zhanghai.android.files.provider.common.toByteString -import me.zhanghai.android.files.provider.ftp.client.Authority -import me.zhanghai.android.files.util.readParcelable -import java.io.IOException - -internal class FtpFileSystem( - private val provider: FtpFileSystemProvider, - val authority: Authority -) : FileSystem(), ByteStringListPathCreator, Parcelable { - val rootDirectory = FtpPath(this, SEPARATOR_BYTE_STRING) - - init { - if (!rootDirectory.isAbsolute) { - throw AssertionError("Root directory must be absolute") - } - if (rootDirectory.nameCount != 0) { - throw AssertionError("Root directory must contain no names") - } - } - - private val lock = Any() - - private var isOpen = true - - val defaultDirectory: FtpPath - get() = rootDirectory - - override fun provider(): FileSystemProvider = provider - - override fun close() { - synchronized(lock) { - if (!isOpen) { - return - } - provider.removeFileSystem(this) - isOpen = false - } - } - - override fun isOpen(): Boolean = synchronized(lock) { isOpen } - - override fun isReadOnly(): Boolean = false - - override fun getSeparator(): String = SEPARATOR_STRING - - override fun getRootDirectories(): Iterable = listOf(rootDirectory) - - override fun getFileStores(): Iterable { - // TODO - throw UnsupportedOperationException() - } - - override fun supportedFileAttributeViews(): Set = - FtpFileAttributeView.SUPPORTED_NAMES - - override fun getPath(first: String, vararg more: String): FtpPath { - val path = ByteStringBuilder(first.toByteString()) - .apply { more.forEach { append(SEPARATOR).append(it.toByteString()) } } - .toByteString() - return FtpPath(this, path) - } - - override fun getPath(first: ByteString, vararg more: ByteString): FtpPath { - val path = ByteStringBuilder(first) - .apply { more.forEach { append(SEPARATOR).append(it) } } - .toByteString() - return FtpPath(this, path) - } - - override fun getPathMatcher(syntaxAndPattern: String): PathMatcher { - throw UnsupportedOperationException() - } - - override fun getUserPrincipalLookupService(): UserPrincipalLookupService { - throw UnsupportedOperationException() - } - - @Throws(IOException::class) - override fun newWatchService(): WatchService = PollingWatchService() - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - if (javaClass != other?.javaClass) { - return false - } - other as FtpFileSystem - return authority == other.authority - } - - override fun hashCode(): Int = authority.hashCode() - - override fun describeContents(): Int = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeParcelable(authority, flags) - } - - companion object { - const val SEPARATOR = '/'.code.toByte() - private val SEPARATOR_BYTE_STRING = SEPARATOR.toByteString() - private const val SEPARATOR_STRING = SEPARATOR.toInt().toChar().toString() - - @JvmField - val CREATOR = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): FtpFileSystem { - val authority = source.readParcelable()!! - return FtpFileSystemProvider.getOrNewFileSystem(authority) - } - - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystemProvider.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystemProvider.kt deleted file mode 100644 index 67d27f467..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystemProvider.kt +++ /dev/null @@ -1,431 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp - -import android.net.Uri -import java8.nio.channels.FileChannel -import java8.nio.channels.SeekableByteChannel -import java8.nio.file.AccessMode -import java8.nio.file.CopyOption -import java8.nio.file.DirectoryStream -import java8.nio.file.FileAlreadyExistsException -import java8.nio.file.FileStore -import java8.nio.file.FileSystem -import java8.nio.file.FileSystemAlreadyExistsException -import java8.nio.file.FileSystemException -import java8.nio.file.FileSystemNotFoundException -import java8.nio.file.LinkOption -import java8.nio.file.NoSuchFileException -import java8.nio.file.NotLinkException -import java8.nio.file.OpenOption -import java8.nio.file.Path -import java8.nio.file.ProviderMismatchException -import java8.nio.file.StandardOpenOption -import java8.nio.file.attribute.BasicFileAttributes -import java8.nio.file.attribute.FileAttribute -import java8.nio.file.attribute.FileAttributeView -import java8.nio.file.spi.FileSystemProvider -import me.zhanghai.android.files.provider.common.ByteStringPath -import me.zhanghai.android.files.provider.common.DelegateSchemeFileSystemProvider -import me.zhanghai.android.files.provider.common.PathListDirectoryStream -import me.zhanghai.android.files.provider.common.PathObservable -import me.zhanghai.android.files.provider.common.PathObservableProvider -import me.zhanghai.android.files.provider.common.Searchable -import me.zhanghai.android.files.provider.common.WalkFileTreeSearchable -import me.zhanghai.android.files.provider.common.WatchServicePathObservable -import me.zhanghai.android.files.provider.common.decodedPathByteString -import me.zhanghai.android.files.provider.common.decodedQueryByteString -import me.zhanghai.android.files.provider.common.toAccessModes -import me.zhanghai.android.files.provider.common.toByteString -import me.zhanghai.android.files.provider.common.toCopyOptions -import me.zhanghai.android.files.provider.common.toLinkOptions -import me.zhanghai.android.files.provider.common.toOpenOptions -import me.zhanghai.android.files.provider.ftp.client.Authority -import me.zhanghai.android.files.provider.ftp.client.Client -import me.zhanghai.android.files.provider.ftp.client.Mode -import me.zhanghai.android.files.provider.ftp.client.Protocol -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.net.URI - -object FtpFileSystemProvider : FileSystemProvider(), PathObservableProvider, Searchable { - private val HIDDEN_FILE_NAME_PREFIX = ".".toByteString() - - private val fileSystems = mutableMapOf() - - private val lock = Any() - - override fun getScheme(): String = Protocol.FTP.scheme - - override fun newFileSystem(uri: URI, env: Map): FileSystem { - uri.requireSameScheme() - val authority = uri.ftpAuthority - synchronized(lock) { - if (fileSystems[authority] != null) { - throw FileSystemAlreadyExistsException(authority.toString()) - } - return newFileSystemLocked(authority) - } - } - - internal fun getOrNewFileSystem(authority: Authority): FtpFileSystem = - synchronized(lock) { fileSystems[authority] ?: newFileSystemLocked(authority) } - - private fun newFileSystemLocked(authority: Authority): FtpFileSystem { - val fileSystem = FtpFileSystem(this, authority) - fileSystems[authority] = fileSystem - return fileSystem - } - - override fun getFileSystem(uri: URI): FileSystem { - uri.requireSameScheme() - val authority = uri.ftpAuthority - return synchronized(lock) { fileSystems[authority] } - ?: throw FileSystemNotFoundException(authority.toString()) - } - - internal fun removeFileSystem(fileSystem: FtpFileSystem) { - val authority = fileSystem.authority - synchronized(lock) { fileSystems.remove(authority) } - } - - override fun getPath(uri: URI): Path { - uri.requireSameScheme() - val authority = uri.ftpAuthority - val path = uri.decodedPathByteString - ?: throw IllegalArgumentException("URI must have a path") - return getOrNewFileSystem(authority).getPath(path) - } - - private fun URI.requireSameScheme() { - val scheme = scheme - require(scheme in Protocol.SCHEMES) { "URI scheme $scheme must be in ${Protocol.SCHEMES}" } - } - - private val URI.ftpAuthority: Authority - get() { - val protocol = Protocol.fromScheme(scheme) - val port = if (port != -1) port else protocol.defaultPort - val username = userInfo ?: "" - val queryUri = decodedQueryByteString?.toString()?.let { Uri.parse(it) } - val mode = queryUri?.getQueryParameter(FtpPath.QUERY_PARAMETER_MODE) - ?.let { mode -> Mode.values().first { it.name.equals(mode, true) } } - ?: Authority.DEFAULT_MODE - val encoding = queryUri?.getQueryParameter(FtpPath.QUERY_PARAMETER_ENCODING) - ?: Authority.DEFAULT_ENCODING - return Authority(protocol, host, port, username, mode, encoding) - } - - @Throws(IOException::class) - override fun newInputStream(file: Path, vararg options: OpenOption): InputStream { - file as? FtpPath ?: throw ProviderMismatchException(file.toString()) - val openOptions = options.toOpenOptions() - openOptions.checkForFtp() - if (openOptions.write) { - throw UnsupportedOperationException(StandardOpenOption.WRITE.toString()) - } - if (openOptions.append) { - throw UnsupportedOperationException(StandardOpenOption.APPEND.toString()) - } - if (openOptions.truncateExisting) { - throw UnsupportedOperationException(StandardOpenOption.TRUNCATE_EXISTING.toString()) - } - if (openOptions.create || openOptions.createNew || openOptions.noFollowLinks) { - val fileFile = try { - Client.listFileOrNull(file, true) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(file.toString()) - } - if (openOptions.noFollowLinks && fileFile != null && fileFile.isSymbolicLink) { - throw FileSystemException( - file.toString(), null, "File is a symbolic link: $fileFile" - ) - } - if (openOptions.createNew && fileFile != null) { - throw FileAlreadyExistsException(file.toString()) - } - if ((openOptions.create || openOptions.createNew) && fileFile == null) { - try { - Client.createFile(file) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(file.toString()) - } - } - } - try { - return Client.retrieveFile(file) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(file.toString()) - } - } - - @Throws(IOException::class) - override fun newOutputStream(file: Path, vararg options: OpenOption): OutputStream { - file as? FtpPath ?: throw ProviderMismatchException(file.toString()) - val optionsSet = mutableSetOf(*options) - if (optionsSet.isEmpty()) { - optionsSet += StandardOpenOption.CREATE - optionsSet += StandardOpenOption.TRUNCATE_EXISTING - } - optionsSet += StandardOpenOption.WRITE - val openOptions = optionsSet.toOpenOptions() - openOptions.checkForFtp() - if (!openOptions.truncateExisting && !openOptions.createNew) { - throw UnsupportedOperationException("Missing ${StandardOpenOption.TRUNCATE_EXISTING}") - } - val fileFile = try { - Client.listFileOrNull(file, true) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(file.toString()) - } - if (openOptions.createNew && fileFile != null) { - throw FileAlreadyExistsException(file.toString()) - } - if (!(openOptions.create || openOptions.createNew) && fileFile == null) { - throw NoSuchFileException(file.toString()) - } - try { - return Client.storeFile(file) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(file.toString()) - } - } - - @Throws(IOException::class) - override fun newFileChannel( - file: Path, - options: Set, - vararg attributes: FileAttribute<*> - ): FileChannel { - file as? FtpPath ?: throw ProviderMismatchException(file.toString()) - options.toOpenOptions().checkForFtp() - if (attributes.isNotEmpty()) { - throw UnsupportedOperationException(attributes.contentToString()) - } - throw UnsupportedOperationException() - } - - @Throws(IOException::class) - override fun newByteChannel( - file: Path, - options: Set, - vararg attributes: FileAttribute<*> - ): SeekableByteChannel { - file as? FtpPath ?: throw ProviderMismatchException(file.toString()) - val openOptions = options.toOpenOptions() - openOptions.checkForFtp() - if (openOptions.write && !openOptions.truncateExisting) { - throw UnsupportedOperationException("Missing ${StandardOpenOption.TRUNCATE_EXISTING}") - } - if (attributes.isNotEmpty()) { - throw UnsupportedOperationException(attributes.contentToString()) - } - try { - return Client.openByteChannel(file, openOptions.append) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(file.toString()) - } - } - - @Throws(IOException::class) - override fun newDirectoryStream( - directory: Path, - filter: DirectoryStream.Filter - ): DirectoryStream { - directory as? FtpPath ?: throw ProviderMismatchException(directory.toString()) - val paths = try { - @Suppress("UNCHECKED_CAST") - Client.listDirectory(directory) as List - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(directory.toString()) - } - return PathListDirectoryStream(paths, filter) - } - - @Throws(IOException::class) - override fun createDirectory(directory: Path, vararg attributes: FileAttribute<*>) { - directory as? FtpPath ?: throw ProviderMismatchException(directory.toString()) - if (attributes.isNotEmpty()) { - throw UnsupportedOperationException(attributes.contentToString()) - } - try { - Client.createDirectory(directory) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(directory.toString()) - } - } - - override fun createSymbolicLink(link: Path, target: Path, vararg attributes: FileAttribute<*>) { - link as? FtpPath ?: throw ProviderMismatchException(link.toString()) - when (target) { - is FtpPath, is ByteStringPath -> {} - else -> throw ProviderMismatchException(target.toString()) - } - if (attributes.isNotEmpty()) { - throw UnsupportedOperationException(attributes.contentToString()) - } - throw UnsupportedOperationException() - } - - override fun createLink(link: Path, existing: Path) { - link as? FtpPath ?: throw ProviderMismatchException(link.toString()) - existing as? FtpPath ?: throw ProviderMismatchException(existing.toString()) - throw UnsupportedOperationException() - } - - @Throws(IOException::class) - override fun delete(path: Path) { - path as? FtpPath ?: throw ProviderMismatchException(path.toString()) - try { - Client.delete(path) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(path.toString()) - } - } - - override fun readSymbolicLink(link: Path): Path { - link as? FtpPath ?: throw ProviderMismatchException(link.toString()) - val linkFile = try { - Client.listFile(link, true) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(link.toString()) - } - if (!linkFile.isSymbolicLink) { - throw NotLinkException(link.toString(), null, linkFile.toString()) - } - val target = linkFile.link ?: throw FileSystemException( - link.toString(), null, "FTPFile.getLink() returned null: $linkFile" - ) - return ByteStringPath(target.toByteString()) - } - - @Throws(IOException::class) - override fun copy(source: Path, target: Path, vararg options: CopyOption) { - source as? FtpPath ?: throw ProviderMismatchException(source.toString()) - target as? FtpPath ?: throw ProviderMismatchException(target.toString()) - val copyOptions = options.toCopyOptions() - FtpCopyMove.copy(source, target, copyOptions) - } - - @Throws(IOException::class) - override fun move(source: Path, target: Path, vararg options: CopyOption) { - source as? FtpPath ?: throw ProviderMismatchException(source.toString()) - target as? FtpPath ?: throw ProviderMismatchException(target.toString()) - val copyOptions = options.toCopyOptions() - FtpCopyMove.move(source, target, copyOptions) - } - - override fun isSameFile(path: Path, path2: Path): Boolean { - path as? FtpPath ?: throw ProviderMismatchException(path.toString()) - return path == path2 - } - - override fun isHidden(path: Path): Boolean { - path as? FtpPath ?: throw ProviderMismatchException(path.toString()) - val fileName = path.fileNameByteString ?: return false - return fileName.startsWith(HIDDEN_FILE_NAME_PREFIX) - } - - override fun getFileStore(path: Path): FileStore { - path as? FtpPath ?: throw ProviderMismatchException(path.toString()) - throw UnsupportedOperationException() - } - - @Throws(IOException::class) - override fun checkAccess(path: Path, vararg modes: AccessMode) { - path as? FtpPath ?: throw ProviderMismatchException(path.toString()) - val accessModes = modes.toAccessModes() - if (accessModes.write) { - throw UnsupportedOperationException(AccessMode.WRITE.toString()) - } - if (accessModes.execute) { - throw UnsupportedOperationException(AccessMode.EXECUTE.toString()) - } - // Assume the file can be read if it can be listed. - try { - Client.listFile(path, false) - } catch (e: IOException) { - throw e.toFileSystemExceptionForFtp(path.toString()) - } - } - - override fun getFileAttributeView( - path: Path, - type: Class, - vararg options: LinkOption - ): V? { - if (!supportsFileAttributeView(type)) { - return null - } - @Suppress("UNCHECKED_CAST") - return getFileAttributeView(path, *options) as V - } - - internal fun supportsFileAttributeView(type: Class): Boolean = - type.isAssignableFrom(FtpFileAttributeView::class.java) - - @Throws(IOException::class) - override fun readAttributes( - path: Path, - type: Class, - vararg options: LinkOption - ): A { - if (!type.isAssignableFrom(BasicFileAttributes::class.java)) { - throw UnsupportedOperationException(type.toString()) - } - @Suppress("UNCHECKED_CAST") - return getFileAttributeView(path, *options).readAttributes() as A - } - - private fun getFileAttributeView(path: Path, vararg options: LinkOption): FtpFileAttributeView { - path as? FtpPath ?: throw ProviderMismatchException(path.toString()) - val linkOptions = options.toLinkOptions() - return FtpFileAttributeView(path, linkOptions.noFollowLinks) - } - - override fun readAttributes( - path: Path, - attributes: String, - vararg options: LinkOption - ): Map { - path as? FtpPath ?: throw ProviderMismatchException(path.toString()) - throw UnsupportedOperationException() - } - - override fun setAttribute( - path: Path, - attribute: String, - value: Any, - vararg options: LinkOption - ) { - path as? FtpPath ?: throw ProviderMismatchException(path.toString()) - throw UnsupportedOperationException() - } - - @Throws(IOException::class) - override fun observe(path: Path, intervalMillis: Long): PathObservable { - path as? FtpPath ?: throw ProviderMismatchException(path.toString()) - return WatchServicePathObservable(path, intervalMillis) - } - - @Throws(IOException::class) - override fun search( - directory: Path, - query: String, - intervalMillis: Long, - listener: (List) -> Unit - ) { - directory as? FtpPath ?: throw ProviderMismatchException(directory.toString()) - WalkFileTreeSearchable.search(directory, query, intervalMillis, listener) - } -} - -val FtpsFileSystemProvider = - DelegateSchemeFileSystemProvider(Protocol.FTPS.scheme, FtpFileSystemProvider) - -val FtpesFileSystemProvider = - DelegateSchemeFileSystemProvider(Protocol.FTPES.scheme, FtpFileSystemProvider) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpPath.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpPath.kt deleted file mode 100644 index 8f57969fd..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpPath.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp - -import android.net.Uri -import android.os.Parcel -import android.os.Parcelable -import java8.nio.file.FileSystem -import java8.nio.file.LinkOption -import java8.nio.file.Path -import java8.nio.file.ProviderMismatchException -import java8.nio.file.WatchEvent -import java8.nio.file.WatchKey -import java8.nio.file.WatchService -import me.zhanghai.android.files.provider.common.ByteString -import me.zhanghai.android.files.provider.common.ByteStringListPath -import me.zhanghai.android.files.provider.common.PollingWatchService -import me.zhanghai.android.files.provider.common.UriAuthority -import me.zhanghai.android.files.provider.common.toByteString -import me.zhanghai.android.files.provider.ftp.client.Authority -import me.zhanghai.android.files.provider.ftp.client.Client -import me.zhanghai.android.files.util.readParcelable -import java.io.File -import java.io.IOException - -internal class FtpPath : ByteStringListPath, Client.Path { - private val fileSystem: FtpFileSystem - - constructor( - fileSystem: FtpFileSystem, - path: ByteString - ) : super(FtpFileSystem.SEPARATOR, path) { - this.fileSystem = fileSystem - } - - private constructor( - fileSystem: FtpFileSystem, - absolute: Boolean, - segments: List - ) : super(FtpFileSystem.SEPARATOR, absolute, segments) { - this.fileSystem = fileSystem - } - - override fun isPathAbsolute(path: ByteString): Boolean = - path.isNotEmpty() && path[0] == FtpFileSystem.SEPARATOR - - override fun createPath(path: ByteString): FtpPath = FtpPath(fileSystem, path) - - override fun createPath(absolute: Boolean, segments: List): FtpPath = - FtpPath(fileSystem, absolute, segments) - - override val uriScheme: String - get() = fileSystem.authority.protocol.scheme - - override val uriAuthority: UriAuthority - get() = fileSystem.authority.toUriAuthority() - - override val uriQuery: ByteString? - get() = - Uri.Builder().apply { - val authority = fileSystem.authority - if (authority.mode != Authority.DEFAULT_MODE) { - appendQueryParameter(QUERY_PARAMETER_MODE, authority.mode.name.lowercase()) - } - if (authority.encoding != Authority.DEFAULT_ENCODING) { - appendQueryParameter(QUERY_PARAMETER_ENCODING, authority.encoding) - } - }.build().query?.toByteString() - - override val defaultDirectory: FtpPath - get() = fileSystem.defaultDirectory - - override fun getFileSystem(): FileSystem = fileSystem - - override fun getRoot(): FtpPath? = if (isAbsolute) fileSystem.rootDirectory else null - - @Throws(IOException::class) - override fun toRealPath(vararg options: LinkOption): FtpPath { - throw UnsupportedOperationException() - } - - override fun toFile(): File { - throw UnsupportedOperationException() - } - - @Throws(IOException::class) - override fun register( - watcher: WatchService, - events: Array>, - vararg modifiers: WatchEvent.Modifier - ): WatchKey { - if (watcher !is PollingWatchService) { - throw ProviderMismatchException(watcher.toString()) - } - return watcher.register(this, events, *modifiers) - } - - override val authority: Authority - get() = fileSystem.authority - - override val remotePath: String - get() = toString() - - private constructor(source: Parcel) : super(source) { - fileSystem = source.readParcelable()!! - } - - override fun writeToParcel(dest: Parcel, flags: Int) { - super.writeToParcel(dest, flags) - - dest.writeParcelable(fileSystem, flags) - } - - companion object { - @JvmField - val CREATOR = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): FtpPath = FtpPath(source) - - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - - const val QUERY_PARAMETER_MODE = "mode" - const val QUERY_PARAMETER_ENCODING = "encoding" - } -} - -val Path.isFtpPath: Boolean - get() = this is FtpPath diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/IOExceptionFtpExtensions.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/IOExceptionFtpExtensions.kt deleted file mode 100644 index fdc43853f..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/IOExceptionFtpExtensions.kt +++ /dev/null @@ -1,16 +0,0 @@ -package me.zhanghai.android.files.provider.ftp - -import java8.nio.file.FileSystemException -import me.zhanghai.android.files.provider.ftp.client.NegativeReplyCodeException -import java.io.IOException - -fun IOException.toFileSystemExceptionForFtp( - file: String?, - other: String? = null -): FileSystemException = - when (this) { - is NegativeReplyCodeException -> toFileSystemException(file, other) - else -> - FileSystemException(file, other, message) - .apply { initCause(this@toFileSystemExceptionForFtp) } - } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/OpenOptionsFtpExtensions.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/OpenOptionsFtpExtensions.kt deleted file mode 100644 index a2515da30..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/OpenOptionsFtpExtensions.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp - -import java8.nio.file.StandardOpenOption -import me.zhanghai.android.files.provider.common.OpenOptions - -internal fun OpenOptions.checkForFtp() { - if (deleteOnClose) { - throw UnsupportedOperationException(StandardOpenOption.DELETE_ON_CLOSE.toString()) - } - if (sync) { - throw UnsupportedOperationException(StandardOpenOption.SYNC.toString()) - } - if (dsync) { - throw UnsupportedOperationException(StandardOpenOption.DSYNC.toString()) - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/PathFtpExtensions.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/PathFtpExtensions.kt deleted file mode 100644 index b076957d2..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/PathFtpExtensions.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp - -import java8.nio.file.Path -import me.zhanghai.android.files.provider.ftp.client.Authority - -fun Authority.createFtpRootPath(): Path = - FtpFileSystemProvider.getOrNewFileSystem(this).rootDirectory diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Authenticator.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Authenticator.kt deleted file mode 100644 index 08f09e489..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Authenticator.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp.client - -interface Authenticator { - fun getPassword(authority: Authority): String? -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Authority.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Authority.kt deleted file mode 100644 index ebe008d62..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Authority.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp.client - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import me.zhanghai.android.files.provider.common.UriAuthority -import me.zhanghai.android.files.util.takeIfNotEmpty -import java.nio.charset.StandardCharsets - -@Parcelize -data class Authority( - val protocol: Protocol, - val host: String, - val port: Int, - val username: String, - val mode: Mode, - val encoding: String -) : Parcelable { - fun toUriAuthority(): UriAuthority { - val userInfo = username.takeIfNotEmpty() - val uriPort = port.takeIf { it != protocol.defaultPort } - return UriAuthority(userInfo, host, uriPort) - } - - override fun toString(): String = toUriAuthority().toString() - - companion object { - // @see https://www.rfc-editor.org/rfc/rfc1635 - const val ANONYMOUS_USERNAME = "anonymous" - const val ANONYMOUS_PASSWORD = "guest" - val DEFAULT_MODE = Mode.PASSIVE - val DEFAULT_ENCODING = StandardCharsets.UTF_8.name()!! - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Client.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Client.kt deleted file mode 100644 index d642084e9..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Client.kt +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp.client - -import java8.nio.channels.SeekableByteChannel -import me.zhanghai.android.files.provider.common.DelegateInputStream -import me.zhanghai.android.files.provider.common.DelegateOutputStream -import org.apache.commons.net.ftp.FTPClient -import org.apache.commons.net.ftp.FTPClientConfig -import org.apache.commons.net.ftp.FTPCmd -import org.apache.commons.net.ftp.FTPFile -import org.apache.commons.net.ftp.FTPReply -import org.apache.commons.net.ftp.FTPSClient -import org.threeten.bp.Instant -import org.threeten.bp.ZoneOffset -import org.threeten.bp.chrono.IsoChronology -import org.threeten.bp.format.DateTimeFormatter -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.util.Collections -import java.util.Locale -import java.util.WeakHashMap - -object Client { - private val TIMESTAMP_FORMATTER = - DateTimeFormatter.ofPattern("yyyyMMddHHmmss", Locale.ROOT) - .withChronology(IsoChronology.INSTANCE) - .withZone(ZoneOffset.UTC) - - @Volatile - lateinit var authenticator: Authenticator - - private val clientPool = mutableMapOf>() - - private val directoryFilesCache = Collections.synchronizedMap(WeakHashMap()) - - @Throws(IOException::class) - private fun acquireClient(authority: Authority): FTPClient { - while (true) { - val client = acquireClientUnchecked(authority) ?: break - if (!client.isConnected) { - client.disconnect() - continue - } - val isAlive = try { - client.sendNoOp() - } catch (e: IOException) { - e.printStackTrace() - false - } - if (!isAlive) { - closeClient(client) - continue - } - return client - } - return createClient(authority) - } - - private fun acquireClientUnchecked(authority: Authority): FTPClient? = - synchronized(clientPool) { - val pooledClients = clientPool[authority] ?: return null - pooledClients.removeLastOrNull().also { - if (pooledClients.isEmpty()) { - clientPool -= authority - } - } - } - - @Throws(IOException::class) - private fun createClient(authority: Authority): FTPClient { - val password = authenticator.getPassword(authority) - ?: throw IOException("No password found for $authority") - return authority.protocol.createClient().apply { - configure(FTPClientConfig("")) - // This has to be set before connect(). - controlEncoding = authority.encoding - listHiddenFiles = true - connect(authority.host, authority.port) - try { - if (!FTPReply.isPositiveCompletion(replyCode)) { - throwNegativeReplyCodeException() - } - if (!login(authority.username, password)) { - throwNegativeReplyCodeException() - } - } catch (t: Throwable) { - disconnect() - throw t - } - // This has to be called after connect() despite being entirely local. - if (authority.mode == Mode.PASSIVE) { - enterLocalPassiveMode() - } - try { - if (this is FTPSClient) { - // @see https://datatracker.ietf.org/doc/html/rfc4217#section-9 - execPBSZ(0) - execPROT("P") - } - if (!setFileType(FTPClient.BINARY_FILE_TYPE)) { - throwNegativeReplyCodeException() - } - } catch (t: Throwable) { - closeClient(this) - throw t - } - } - } - - private fun releaseClient(authority: Authority, client: FTPClient) { - if (!client.isConnected) { - client.disconnect() - return - } - // FIXME: Disconnect clients based on time. - if (false) { - closeClient(client) - return - } - synchronized(clientPool) { - clientPool.getOrPut(authority) { mutableListOf() } += client - } - } - - private fun closeClient(client: FTPClient) { - try { - client.logout() - } catch (e: IOException) { - e.printStackTrace() - } - client.disconnect() - } - - private inline fun useClient(authority: Authority, block: (FTPClient) -> R): R { - val client = acquireClient(authority) - try { - return block(client) - } finally { - releaseClient(authority, client) - } - } - - @Throws(IOException::class) - fun createDirectory(path: Path) { - useClient(path.authority) { client -> - if (!client.makeDirectory(path.remotePath)) { - client.throwNegativeReplyCodeException() - } - } - } - - @Throws(IOException::class) - fun createFile(path: Path) { - storeFile(path).close() - } - - @Throws(IOException::class) - fun delete(path: Path) { - val file = listFile(path, true) - delete(path, file.isDirectory) - } - - @Throws(IOException::class) - fun delete(path: Path, isDirectory: Boolean) { - if (isDirectory) { - deleteDirectory(path) - } else { - deleteFile(path) - } - } - - @Throws(IOException::class) - fun deleteFile(path: Path) { - useClient(path.authority) { client -> - if (!client.deleteFile(path.remotePath)) { - client.throwNegativeReplyCodeException() - } - } - directoryFilesCache -= path - } - - @Throws(IOException::class) - fun deleteDirectory(path: Path) { - useClient(path.authority) { client -> - if (!client.removeDirectory(path.remotePath)) { - client.throwNegativeReplyCodeException() - } - } - directoryFilesCache -= path - } - - @Throws(IOException::class) - fun renameFile(source: Path, target: Path) { - if (source.authority != target.authority) { - throw IOException("Paths aren't on the same authority") - } - useClient(source.authority) { client -> - if (!client.rename(source.remotePath, target.remotePath)) { - client.throwNegativeReplyCodeException() - } - } - directoryFilesCache -= source - directoryFilesCache -= target - } - - @Throws(IOException::class) - fun retrieveFile(path: Path): InputStream { - val authority = path.authority - val client = acquireClient(authority) - val inputStream = try { - client.retrieveFileStream(path.remotePath) ?: client.throwNegativeReplyCodeException() - } catch (t: Throwable) { - releaseClient(authority, client) - throw t - } - return CompletePendingCommandInputStream(inputStream, authority, client) - } - - @Throws(IOException::class) - fun listDirectory(path: Path): List { - useClient(path.authority) { client -> - val files = client.mlistDirCompat(path.remotePath) - ?: client.throwNegativeReplyCodeException() - return files.mapNotNull { file -> - if (file.name == "." || file.name == "..") { - return@mapNotNull null - } - path.resolve(file.name).also { directoryFilesCache[it] = file } - } - } - } - - @Throws(IOException::class) - fun listFileOrNull(path: Path, noFollowLinks: Boolean): FTPFile? = - try { - listFile(path, noFollowLinks) - } catch (e: NegativeReplyCodeException) { - null - } - - @Throws(IOException::class) - fun listFile(path: Path, noFollowLinks: Boolean): FTPFile { - val file = listFileNoFollowLinks(path, noFollowLinks) - if (!file.isSymbolicLink || noFollowLinks) { - return file - } - val targetString = file.link ?: throw IOException("FTPFile.getLink() returned null: $file") - val target = path.resolve(targetString) - return listFileNoFollowLinks(target, false) - } - - @Throws(IOException::class) - private fun listFileNoFollowLinks(path: Path, preserveCacheForSymbolicLink: Boolean): FTPFile { - synchronized(directoryFilesCache) { - directoryFilesCache[path]?.let { - if (!(it.isSymbolicLink && preserveCacheForSymbolicLink)) { - directoryFilesCache -= path - } - return it - } - } - useClient(path.authority) { client -> - return client.mlistFileCompat(path.remotePath) - ?: client.throwNegativeReplyCodeException() - } - } - - @Throws(IOException::class) - fun openByteChannel(path: Path, isAppend: Boolean): SeekableByteChannel { - val authority = path.authority - val client = acquireClient(authority) - if (!client.hasFeature(FTPCmd.REST)) { - throw IOException("Missing feature ${FTPCmd.REST.command}") - } - return FileByteChannel( - client, { releaseClient(authority, client) }, path.remotePath, isAppend - ) - } - - @Throws(IOException::class) - fun setLastModifiedTime(path: Path, lastModifiedTime: Instant) { - val lastModifiedTimeString = TIMESTAMP_FORMATTER.format(lastModifiedTime) - useClient(path.authority) { client -> - if (!client.setModificationTimeCompat(path.remotePath, lastModifiedTimeString)) { - client.throwNegativeReplyCodeException() - } - } - } - - @Throws(IOException::class) - fun storeFile(path: Path): OutputStream { - val authority = path.authority - val client = acquireClient(authority) - val outputStream = try { - client.storeFileStream(path.remotePath) ?: client.throwNegativeReplyCodeException() - } catch (t: Throwable) { - releaseClient(authority, client) - throw t - } - return CompletePendingCommandOutputStream(outputStream, authority, client) - } - - interface Path { - val authority: Authority - val remotePath: String - fun resolve(other: String): Path - } - - private class CompletePendingCommandInputStream( - inputStream: InputStream, - private val authority: Authority, - private val client: FTPClient - ) : DelegateInputStream(inputStream) { - @Throws(IOException::class) - override fun close() { - try { - super.close() - if (!client.completePendingCommand()) { - // We may close the input stream before the file is fully read (may happen when - // decoding images) and it will result in an error reported here, but that's - // totally fine. - client.createNegativeReplyCodeException().printStackTrace() - } - } finally { - releaseClient(authority, client) - } - } - } - - private class CompletePendingCommandOutputStream( - outputStream: OutputStream, - private val authority: Authority, - private val client: FTPClient - ) : DelegateOutputStream(outputStream) { - @Throws(IOException::class) - override fun close() { - try { - super.close() - if (!client.completePendingCommand()) { - client.throwNegativeReplyCodeException() - } - } finally { - releaseClient(authority, client) - } - } - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/FTPClientCompat.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/FTPClientCompat.kt deleted file mode 100644 index dcf608cdf..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/FTPClientCompat.kt +++ /dev/null @@ -1,50 +0,0 @@ -package me.zhanghai.android.files.provider.ftp.client - -import org.apache.commons.net.ftp.FTPClient -import org.apache.commons.net.ftp.FTPCmd -import org.apache.commons.net.ftp.FTPFile -import java.io.File -import java.io.IOException -import java.util.Calendar - -private val DUMMY_ROOT_FTP_FILE = FTPFile().apply { - rawListing = "Type=dir;Size=4096;Modify=19700101000000;Perm=cdeflmp; /" - type = FTPFile.DIRECTORY_TYPE - size = 4096 - timestamp = Calendar.getInstance().apply { timeInMillis = 0 } - setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true) - setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true) - setPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, true) - name = "/" -} - -@Throws(IOException::class) -fun FTPClient.mlistFileCompat(pathname: String): FTPFile? { - if (hasFeature(FTPCmd.MLST)) { - return mlistFile(pathname) - } else { - val path = File(pathname) - val parent = path.parent ?: return DUMMY_ROOT_FTP_FILE - return listFiles(parent)?.firstOrNull { it != null && it.name == path.name } - } -} - -@Throws(IOException::class) -fun FTPClient.mlistDirCompat(pathname: String): Array? = - // Note that there is no distinct FEAT output for MLSD. The presence of the MLST feature - // indicates that both MLST and MLSD are supported. - // @see https://datatracker.ietf.org/doc/html/rfc3659#section-7.8 - // FTPClient silently returns an empty array even when server returns an error for unknown - // command, so we have to rely on checking the feature. - if (hasFeature(FTPCmd.MLST)) mlistDir(pathname) else listFiles(pathname) - -@Throws(IOException::class) -fun FTPClient.setModificationTimeCompat(pathname: String, timeval: String): Boolean = - // @see https://www.ietf.org/archive/id/draft-somers-ftp-mfxx-04.txt - // This is frequently called during file operations, so in order to avoid wasting network - // requests, we check the feature first which is cached locally. - if (hasFeature(FTPCmd.MFMT)) { - setModificationTime(pathname, timeval) - } else { - throw IOException("Missing feature ${FTPCmd.MFMT.command}") - } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/FileByteChannel.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/FileByteChannel.kt deleted file mode 100644 index ceb716fb4..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/FileByteChannel.kt +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (c) 2021 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp.client - -import java8.nio.channels.SeekableByteChannel -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.runInterruptible -import kotlinx.coroutines.withTimeout -import me.zhanghai.android.files.provider.common.ForceableChannel -import me.zhanghai.android.files.provider.common.readFully -import me.zhanghai.android.files.util.closeSafe -import org.apache.commons.net.ftp.FTPClient -import java.io.ByteArrayInputStream -import java.io.Closeable -import java.io.IOException -import java.io.InterruptedIOException -import java.nio.ByteBuffer -import java.nio.channels.ClosedChannelException -import java.nio.channels.NonReadableChannelException - -class FileByteChannel( - private val client: FTPClient, - private val releaseClient: (FTPClient) -> Unit, - private val path: String, - private val isAppend: Boolean -) : ForceableChannel, SeekableByteChannel { - private var position = 0L - private val ioLock = Any() - - private val readBuffer = ReadBuffer() - - private var isOpen = true - private val closeLock = Any() - - @Throws(IOException::class) - override fun read(destination: ByteBuffer): Int { - ensureOpen() - if (isAppend) { - throw NonReadableChannelException() - } - val remaining = destination.remaining() - if (remaining == 0) { - return 0 - } - return synchronized(ioLock) { - readBuffer.read(destination).also { - if (it != -1) { - position += it - } - } - } - } - - @Throws(IOException::class) - override fun write(source: ByteBuffer): Int { - ensureOpen() - val remaining = source.remaining() - if (remaining == 0) { - return 0 - } - // I don't think we are using native or read-only ByteBuffer, so just call array() here. - synchronized(ioLock) { - if (isAppend) { - ByteArrayInputStream(source.array(), source.position(), remaining).use { - if (!client.appendFile(path, it)) { - client.throwNegativeReplyCodeException() - } - } - position = getSize() - } else { - client.restartOffset = position - ByteArrayInputStream(source.array(), source.position(), remaining).use { - if (!client.storeFile(path, it)) { - client.throwNegativeReplyCodeException() - } - } - position += remaining - } - source.position(source.limit()) - return remaining - } - } - - @Throws(IOException::class) - override fun position(): Long { - ensureOpen() - synchronized(ioLock) { - if (isAppend) { - position = getSize() - } - return position - } - } - - override fun position(newPosition: Long): SeekableByteChannel { - ensureOpen() - if (isAppend) { - // Ignored. - return this - } - synchronized(ioLock) { - readBuffer.reposition(position, newPosition) - position = newPosition - } - return this - } - - @Throws(IOException::class) - override fun size(): Long { - ensureOpen() - return getSize() - } - - @Throws(IOException::class) - override fun truncate(size: Long): SeekableByteChannel { - ensureOpen() - require(size >= 0) - synchronized(ioLock) { - val currentSize = getSize() - if (size >= currentSize) { - return this - } - client.restartOffset = size - ByteArrayInputStream(byteArrayOf()).use { - if (!client.storeFile(path, it)) { - client.throwNegativeReplyCodeException() - } - } - position = position.coerceAtMost(size) - } - return this - } - - @Throws(IOException::class) - private fun getSize(): Long { - val sizeString = client.getSize(path) ?: client.throwNegativeReplyCodeException() - return sizeString.toLongOrNull() ?: throw IOException("Invalid size $sizeString") - } - - @Throws(IOException::class) - override fun force(metaData: Boolean) { - ensureOpen() - // Unsupported. - } - - @Throws(ClosedChannelException::class) - private fun ensureOpen() { - synchronized(closeLock) { - if (!isOpen) { - throw ClosedChannelException() - } - } - } - - override fun isOpen(): Boolean = synchronized(closeLock) { isOpen } - - @Throws(IOException::class) - override fun close() { - synchronized(closeLock) { - if (!isOpen) { - return - } - isOpen = false - readBuffer.closeSafe() - releaseClient(client) - } - } - - private inner class ReadBuffer : Closeable { - private val bufferSize = DEFAULT_BUFFER_SIZE - private val timeoutMillis = 15_000L - - private val buffer = ByteBuffer.allocate(bufferSize).apply { limit(0) } - private var bufferedPosition = 0L - - private var pendingDeferred: Deferred? = null - private val pendingDeferredLock = Any() - - @Throws(IOException::class) - fun read(destination: ByteBuffer): Int { - if (!buffer.hasRemaining()) { - readIntoBuffer() - if (!buffer.hasRemaining()) { - return -1 - } - } - val length = destination.remaining().coerceAtMost(buffer.remaining()) - val bufferLimit = buffer.limit() - buffer.limit(buffer.position() + length) - destination.put(buffer) - buffer.limit(bufferLimit) - return length - } - - @Throws(IOException::class) - private fun readIntoBuffer() { - val deferred = synchronized(pendingDeferredLock) { - pendingDeferred?.also { pendingDeferred = null } - } ?: readIntoBufferAsync() - val newBuffer = try { - runBlocking { deferred.await() } - } catch (e: CancellationException) { - throw InterruptedIOException().apply { initCause(e) } - } - buffer.clear() - buffer.put(newBuffer) - buffer.flip() - if (!buffer.hasRemaining()) { - return - } - bufferedPosition += buffer.remaining() - synchronized(pendingDeferredLock) { - pendingDeferred = readIntoBufferAsync() - } - } - - private fun readIntoBufferAsync(): Deferred = - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.async(Dispatchers.IO) { - withTimeout(timeoutMillis) { - runInterruptible { - client.restartOffset = bufferedPosition - val inputStream = client.retrieveFileStream(path) - ?: client.throwNegativeReplyCodeException() - val buffer = ByteBuffer.allocate(bufferSize) - val limit = inputStream.use { - it.readFully(buffer.array(), buffer.position(), buffer.remaining()) - } - buffer.limit(limit) - // We may close the input stream before the file is fully read and it will - // result in an error reported here, but that's totally fine. - client.completePendingCommand() - buffer - } - } - } - - fun reposition(oldPosition: Long, newPosition: Long) { - if (newPosition == oldPosition) { - return - } - val newBufferPosition = buffer.position() + (newPosition - oldPosition) - if (newBufferPosition in 0..buffer.limit()) { - buffer.position(newBufferPosition.toInt()) - } else { - synchronized(pendingDeferredLock) { - pendingDeferred?.let { - it.cancel() - pendingDeferred = null - } - } - buffer.limit(0) - bufferedPosition = newPosition - } - } - - override fun close() { - synchronized(pendingDeferredLock) { - pendingDeferred?.let { - it.cancel() - pendingDeferred = null - } - } - } - } - - companion object { - private const val DEFAULT_BUFFER_SIZE = 1024 * 1024 - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Mode.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Mode.kt deleted file mode 100644 index 96d771890..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Mode.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp.client - -enum class Mode { - ACTIVE, - PASSIVE; -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/NegativeReplyCodeException.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/NegativeReplyCodeException.kt deleted file mode 100644 index cedba4895..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/NegativeReplyCodeException.kt +++ /dev/null @@ -1,30 +0,0 @@ -package me.zhanghai.android.files.provider.ftp.client - -import java8.nio.file.AccessDeniedException -import java8.nio.file.FileSystemException -import java8.nio.file.NoSuchFileException -import me.zhanghai.android.files.provider.common.InvalidFileNameException -import org.apache.commons.net.ftp.FTPClient -import org.apache.commons.net.ftp.FTPReply -import java.io.IOException - -class NegativeReplyCodeException( - private val replyCode: Int, - replyString: String -) : IOException(replyString) { - fun toFileSystemException(file: String?, other: String? = null): FileSystemException = - when (replyCode) { - FTPReply.NOT_LOGGED_IN, FTPReply.NEED_ACCOUNT_FOR_STORING_FILES -> - AccessDeniedException(file, other, message) - FTPReply.FILE_UNAVAILABLE -> NoSuchFileException(file, other, message) - FTPReply.FILE_NAME_NOT_ALLOWED -> InvalidFileNameException(file, other, message) - else -> FileSystemException(file, other, message) - }.apply { initCause(this@NegativeReplyCodeException) } -} - -internal fun FTPClient.createNegativeReplyCodeException() = - NegativeReplyCodeException(replyCode, replyString) - -internal fun FTPClient.throwNegativeReplyCodeException(): Nothing { - throw createNegativeReplyCodeException() -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Protocol.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Protocol.kt deleted file mode 100644 index b4026b380..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/client/Protocol.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.ftp.client - -import org.apache.commons.net.ftp.FTPClient -import org.apache.commons.net.ftp.FTPSClient - -enum class Protocol(val scheme: String, val defaultPort: Int, val createClient: () -> FTPClient) { - FTP("ftp", FTPClient.DEFAULT_PORT, ::FTPClient), - FTPS("ftps", FTPSClient.DEFAULT_FTPS_PORT, { FTPSClient(true) }), - FTPES("ftpes", FTPClient.DEFAULT_PORT, { FTPSClient(false) }); - - companion object { - val SCHEMES = values().map { it.scheme } - - fun fromScheme(scheme: String): Protocol = - values().firstOrNull() { it.scheme == scheme } ?: throw IllegalArgumentException(scheme) - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/AddStorageDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/AddStorageDialogFragment.kt index 3baf24178..205658e4c 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/AddStorageDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/AddStorageDialogFragment.kt @@ -65,8 +65,6 @@ class AddStorageDialogFragment : AppCompatDialogFragment() { R.string.storage_add_storage_document_tree to AddDocumentTreeActivity::class.createIntent() .putArgs(AddDocumentTreeFragment.Args(null, null)), - R.string.storage_add_storage_ftp_server to EditFtpServerActivity::class.createIntent() - .putArgs(EditFtpServerFragment.Args()), R.string.storage_add_storage_sftp_server to EditSftpServerActivity::class.createIntent() .putArgs(EditSftpServerFragment.Args()), R.string.storage_add_storage_smb_server to AddLanSmbServerActivity::class.createIntent() diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerActivity.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerActivity.kt deleted file mode 100644 index 0e6174355..000000000 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerActivity.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.storage - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.commit -import me.zhanghai.android.files.app.AppActivity -import me.zhanghai.android.files.util.args -import me.zhanghai.android.files.util.putArgs - -class EditFtpServerActivity : AppActivity() { - private val args by args() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Calls ensureSubDecor(). - findViewById(android.R.id.content) - if (savedInstanceState == null) { - val fragment = EditFtpServerFragment().putArgs(args) - supportFragmentManager.commit { add(android.R.id.content, fragment) } - } - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt deleted file mode 100644 index b8982c5a3..000000000 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.storage - -import android.app.Activity -import android.os.Bundle -import android.text.TextUtils -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible -import androidx.core.widget.doAfterTextChanged -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.google.android.material.textfield.TextInputEditText -import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize -import me.zhanghai.android.files.R -import me.zhanghai.android.files.databinding.EditFtpServerFragmentBinding -import me.zhanghai.android.files.provider.ftp.client.Authority -import me.zhanghai.android.files.provider.ftp.client.Mode -import me.zhanghai.android.files.provider.ftp.client.Protocol -import me.zhanghai.android.files.ui.UnfilteredArrayAdapter -import me.zhanghai.android.files.util.ActionState -import me.zhanghai.android.files.util.ParcelableArgs -import me.zhanghai.android.files.util.args -import me.zhanghai.android.files.util.fadeToVisibilityUnsafe -import me.zhanghai.android.files.util.finish -import me.zhanghai.android.files.util.getTextArray -import me.zhanghai.android.files.util.hideTextInputLayoutErrorOnTextChange -import me.zhanghai.android.files.util.isReady -import me.zhanghai.android.files.util.setResult -import me.zhanghai.android.files.util.showToast -import me.zhanghai.android.files.util.takeIfNotEmpty -import me.zhanghai.android.files.util.viewModels -import java.net.URI - -class EditFtpServerFragment : Fragment() { - private val args by args() - - private val viewModel by viewModels { { EditFtpServerViewModel() } } - - private lateinit var binding: EditFtpServerFragmentBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - lifecycleScope.launchWhenStarted { - launch { viewModel.connectState.collect { onConnectStateChanged(it) } } - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = - EditFtpServerFragmentBinding.inflate(inflater, container, false) - .also { binding = it } - .root - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val activity = requireActivity() as AppCompatActivity - activity.lifecycleScope.launchWhenCreated { - activity.setSupportActionBar(binding.toolbar) - activity.supportActionBar!!.setDisplayHomeAsUpEnabled(true) - activity.setTitle( - if (args.server != null) { - R.string.storage_edit_ftp_server_title_edit - } else { - R.string.storage_edit_ftp_server_title_add - } - ) - } - - binding.hostEdit.hideTextInputLayoutErrorOnTextChange(binding.hostLayout) - binding.hostEdit.doAfterTextChanged { updateNamePlaceholder() } - binding.portEdit.hideTextInputLayoutErrorOnTextChange(binding.portLayout) - binding.portEdit.doAfterTextChanged { updateNamePlaceholder() } - binding.pathEdit.doAfterTextChanged { updateNamePlaceholder() } - binding.protocolEdit.setAdapter( - UnfilteredArrayAdapter( - binding.protocolEdit.context, R.layout.dropdown_item, - objects = getTextArray(R.array.storage_edit_ftp_server_protocol_entries) - ) - ) - protocol = Protocol.FTP - binding.protocolEdit.doAfterTextChanged { - updateNamePlaceholder() - updatePortPlaceholder() - } - binding.authenticationTypeEdit.setAdapter( - UnfilteredArrayAdapter( - binding.authenticationTypeEdit.context, R.layout.dropdown_item, - objects = getTextArray(R.array.storage_edit_ftp_server_authentication_type_entries) - ) - ) - authenticationType = AuthenticationType.PASSWORD - binding.authenticationTypeEdit.doAfterTextChanged { - onAuthenticationTypeChanged(authenticationType) - updateNamePlaceholder() - } - binding.usernameEdit.hideTextInputLayoutErrorOnTextChange(binding.usernameLayout) - binding.usernameEdit.doAfterTextChanged { updateNamePlaceholder() } - binding.modeEdit.setAdapter( - UnfilteredArrayAdapter( - binding.modeEdit.context, R.layout.dropdown_item, - objects = getTextArray(R.array.storage_edit_ftp_server_mode_entries) - ) - ) - mode = Authority.DEFAULT_MODE - binding.encodingEdit.setAdapter( - UnfilteredArrayAdapter( - binding.encodingEdit.context, R.layout.dropdown_item, - objects = viewModel.charsets.map { it.displayName() } - ) - ) - encoding = Authority.DEFAULT_ENCODING - binding.saveOrConnectAndAddButton.setText( - if (args.server != null) { - R.string.save - } else { - R.string.storage_edit_ftp_server_connect_and_add - } - ) - binding.saveOrConnectAndAddButton.setOnClickListener { - if (args.server != null) { - saveOrAdd() - } else { - connectAndAdd() - } - } - binding.cancelButton.setOnClickListener { finish() } - binding.removeOrAddButton.setText( - if (args.server != null) R.string.remove else R.string.storage_edit_ftp_server_add - ) - binding.removeOrAddButton.setOnClickListener { - if (args.server != null) { - remove() - } else { - saveOrAdd() - } - } - - if (savedInstanceState == null) { - val server = args.server - if (server != null) { - val authority = server.authority - binding.hostEdit.setText(authority.host) - protocol = authority.protocol - if (authority.port != protocol.defaultPort) { - binding.portEdit.setText(authority.port.toString()) - } - when { - authority.username == Authority.ANONYMOUS_USERNAME - && server.password == Authority.ANONYMOUS_PASSWORD -> - authenticationType = AuthenticationType.ANONYMOUS - else -> { - authenticationType = AuthenticationType.PASSWORD - binding.usernameEdit.setText(authority.username) - binding.passwordEdit.setText(server.password) - } - } - binding.pathEdit.setText(server.relativePath) - binding.nameEdit.setText(server.customName) - mode = authority.mode - encoding = authority.encoding - } else { - val host = args.host - if (host != null) { - binding.hostEdit.setText(host) - } - } - } - } - - private fun updateNamePlaceholder() { - val host = binding.hostEdit.text.toString().takeIfNotEmpty() - val port = binding.portEdit.text.toString().takeIfNotEmpty()?.toIntOrNull() - ?: protocol.defaultPort - val path = binding.pathEdit.text.toString().trim() - val username = when (authenticationType) { - AuthenticationType.PASSWORD -> binding.usernameEdit.text.toString() - AuthenticationType.ANONYMOUS -> Authority.ANONYMOUS_USERNAME - } - binding.nameLayout.placeholderText = if (host != null) { - val authority = Authority(protocol, host, port, username, mode, encoding) - if (path.isNotEmpty()) "$authority/$path" else authority.toString() - } else { - getString(R.string.storage_edit_ftp_server_name_placeholder) - } - } - - private fun updatePortPlaceholder() { - binding.portLayout.placeholderText = protocol.defaultPort.toString() - } - - private var protocol: Protocol - get() { - val adapter = binding.protocolEdit.adapter - val items = List(adapter.count) { adapter.getItem(it) as CharSequence } - val selectedItem = binding.protocolEdit.text - val selectedIndex = items.indexOfFirst { TextUtils.equals(it, selectedItem) } - return Protocol.values()[selectedIndex] - } - set(value) { - val adapter = binding.protocolEdit.adapter - val item = adapter.getItem(value.ordinal) as CharSequence - binding.protocolEdit.setText(item, false) - } - - private var authenticationType: AuthenticationType - get() { - val adapter = binding.authenticationTypeEdit.adapter - val items = List(adapter.count) { adapter.getItem(it) as CharSequence } - val selectedItem = binding.authenticationTypeEdit.text - val selectedIndex = items.indexOfFirst { TextUtils.equals(it, selectedItem) } - return AuthenticationType.values()[selectedIndex] - } - set(value) { - val adapter = binding.authenticationTypeEdit.adapter - val item = adapter.getItem(value.ordinal) as CharSequence - binding.authenticationTypeEdit.setText(item, false) - onAuthenticationTypeChanged(value) - } - - private fun onAuthenticationTypeChanged(authenticationType: AuthenticationType) { - binding.passwordAuthenticationLayout.isVisible = - authenticationType == AuthenticationType.PASSWORD - } - - private var mode: Mode - get() { - val adapter = binding.modeEdit.adapter - val items = List(adapter.count) { adapter.getItem(it) as CharSequence } - val selectedItem = binding.modeEdit.text - val selectedIndex = items.indexOfFirst { TextUtils.equals(it, selectedItem) } - return Mode.values()[selectedIndex] - } - set(value) { - val adapter = binding.modeEdit.adapter - val item = adapter.getItem(value.ordinal) as CharSequence - binding.modeEdit.setText(item, false) - } - - private var encoding: String - get() { - val adapter = binding.encodingEdit.adapter - val items = List(adapter.count) { adapter.getItem(it) as CharSequence } - val selectedItem = binding.encodingEdit.text - val selectedIndex = items.indexOfFirst { TextUtils.equals(it, selectedItem) } - return viewModel.charsets[selectedIndex].name() - } - set(value) { - val adapter = binding.encodingEdit.adapter - val item = adapter.getItem(viewModel.charsets.indexOfFirst { it.name() == value }) - as CharSequence - binding.encodingEdit.setText(item, false) - } - - private fun saveOrAdd() { - val server = getServerOrSetError() ?: return - Storages.addOrReplace(server) - setResult(Activity.RESULT_OK) - finish() - } - - private fun connectAndAdd() { - if (!viewModel.connectState.value.isReady) { - return - } - val server = getServerOrSetError() ?: return - viewModel.connect(server) - } - - private fun onConnectStateChanged(state: ActionState) { - when (state) { - is ActionState.Ready, is ActionState.Running -> { - val isConnecting = state is ActionState.Running - binding.progress.fadeToVisibilityUnsafe(isConnecting) - binding.scrollView.fadeToVisibilityUnsafe(!isConnecting) - binding.saveOrConnectAndAddButton.isEnabled = !isConnecting - binding.removeOrAddButton.isEnabled = !isConnecting - } - is ActionState.Success -> { - Storages.addOrReplace(state.argument) - setResult(Activity.RESULT_OK) - finish() - } - is ActionState.Error -> { - val throwable = state.throwable - throwable.printStackTrace() - showToast(throwable.toString()) - viewModel.finishConnecting() - } - } - } - - private fun remove() { - Storages.remove(args.server!!) - setResult(Activity.RESULT_OK) - finish() - } - - private fun getServerOrSetError(): FtpServer? { - var errorEdit: TextInputEditText? = null - val host = binding.hostEdit.text.toString().takeIfNotEmpty() - if (host == null) { - binding.hostLayout.error = getString(R.string.storage_edit_ftp_server_host_error_empty) - if (errorEdit == null) { - errorEdit = binding.hostEdit - } - } else if (!URI::class.isValidHost(host)) { - binding.hostLayout.error = - getString(R.string.storage_edit_ftp_server_host_error_invalid) - if (errorEdit == null) { - errorEdit = binding.hostEdit - } - } - val port = binding.portEdit.text.toString().takeIfNotEmpty() - .let { if (it != null) it.toIntOrNull() else protocol.defaultPort } - if (port == null) { - binding.portLayout.error = - getString(R.string.storage_edit_ftp_server_port_error_invalid) - if (errorEdit == null) { - errorEdit = binding.portEdit - } - } - val path = binding.pathEdit.text.toString().trim() - val name = binding.nameEdit.text.toString().takeIfNotEmpty() - val username: String? - val password: String - when (authenticationType) { - AuthenticationType.PASSWORD -> { - username = binding.usernameEdit.text.toString().takeIfNotEmpty() - if (username == null) { - binding.usernameLayout.error = - getString(R.string.storage_edit_ftp_server_username_error_empty) - if (errorEdit == null) { - errorEdit = binding.usernameEdit - } - } - password = binding.passwordEdit.text.toString() - } - AuthenticationType.ANONYMOUS -> { - username = Authority.ANONYMOUS_USERNAME - password = Authority.ANONYMOUS_PASSWORD - } - } - if (errorEdit != null) { - errorEdit.requestFocus() - return null - } - val authority = Authority(protocol, host!!, port!!, username!!, mode, encoding) - return FtpServer(args.server?.id, name, authority, password, path) - } - - @Parcelize - class Args( - val server: FtpServer? = null, - val host: String? = null - ) : ParcelableArgs - - private enum class AuthenticationType { - PASSWORD, - ANONYMOUS - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerViewModel.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerViewModel.kt deleted file mode 100644 index edc08a46d..000000000 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerViewModel.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.storage - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.runInterruptible -import me.zhanghai.android.files.provider.common.newDirectoryStream -import me.zhanghai.android.files.util.ActionState -import me.zhanghai.android.files.util.isFinished -import me.zhanghai.android.files.util.isReady -import java.nio.charset.Charset - -class EditFtpServerViewModel : ViewModel() { - val charsets = Charset.availableCharsets().values.toList() - - private val _connectState = MutableStateFlow>(ActionState.Ready()) - val connectState = _connectState.asStateFlow() - - fun connect(server: FtpServer) { - viewModelScope.launch { - check(_connectState.value.isReady) - _connectState.value = ActionState.Running(server) - _connectState.value = try { - runInterruptible(Dispatchers.IO) { - FtpServerAuthenticator.addTransientServer(server) - try { - val path = server.path - path.fileSystem.use { - path.newDirectoryStream().toList() - } - } finally { - FtpServerAuthenticator.removeTransientServer(server) - } - } - ActionState.Success(server, Unit) - } catch (e: Exception) { - ActionState.Error(server, e) - } - } - } - - fun finishConnecting() { - viewModelScope.launch { - check(_connectState.value.isFinished) - _connectState.value = ActionState.Ready() - } - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/FtpServer.kt b/app/src/main/java/me/zhanghai/android/files/storage/FtpServer.kt deleted file mode 100644 index 535481e55..000000000 --- a/app/src/main/java/me/zhanghai/android/files/storage/FtpServer.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.storage - -import android.content.Context -import android.content.Intent -import androidx.annotation.DrawableRes -import java8.nio.file.Path -import kotlinx.parcelize.Parcelize -import me.zhanghai.android.files.R -import me.zhanghai.android.files.provider.ftp.client.Authority -import me.zhanghai.android.files.provider.ftp.createFtpRootPath -import me.zhanghai.android.files.util.createIntent -import me.zhanghai.android.files.util.putArgs -import kotlin.random.Random - -@Parcelize -class FtpServer( - override val id: Long, - override val customName: String?, - val authority: Authority, - val password: String, - val relativePath: String -) : Storage() { - constructor( - id: Long?, - customName: String?, - authority: Authority, - password: String, - relativePath: String - ) : this(id ?: Random.nextLong(), customName, authority, password, relativePath) - - override val iconRes: Int - @DrawableRes - get() = R.drawable.computer_icon_white_24dp - - override fun getDefaultName(context: Context): String = - if (relativePath.isNotEmpty()) "$authority/$relativePath" else authority.toString() - - override val description: String - get() = authority.toString() - - override val path: Path - get() = authority.createFtpRootPath().resolve(relativePath) - - override fun createEditIntent(): Intent = - EditFtpServerActivity::class.createIntent().putArgs(EditFtpServerFragment.Args(this)) -} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/FtpServerAuthenticator.kt b/app/src/main/java/me/zhanghai/android/files/storage/FtpServerAuthenticator.kt deleted file mode 100644 index 65aba49cb..000000000 --- a/app/src/main/java/me/zhanghai/android/files/storage/FtpServerAuthenticator.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.storage - -import me.zhanghai.android.files.provider.ftp.client.Authenticator -import me.zhanghai.android.files.provider.ftp.client.Authority -import me.zhanghai.android.files.settings.Settings -import me.zhanghai.android.files.util.valueCompat - -object FtpServerAuthenticator : Authenticator { - private val transientServers = mutableSetOf() - - override fun getPassword(authority: Authority): String? { - val server = synchronized(transientServers) { - transientServers.find { it.authority == authority } - } ?: Settings.STORAGES.valueCompat.find { - it is FtpServer && it.authority == authority - } as FtpServer? - return server?.password - } - - fun addTransientServer(server: FtpServer) { - synchronized(transientServers) { - transientServers += server - } - } - - fun removeTransientServer(server: FtpServer) { - synchronized(transientServers) { - transientServers -= server - } - } -} diff --git a/app/src/main/res/layout/edit_ftp_server_fragment.xml b/app/src/main/res/layout/edit_ftp_server_fragment.xml deleted file mode 100644 index 9b3884dbe..000000000 --- a/app/src/main/res/layout/edit_ftp_server_fragment.xml +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -