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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/raw/licenses.xml b/app/src/main/res/raw/licenses.xml
index fbbe26496..e36635a99 100644
--- a/app/src/main/res/raw/licenses.xml
+++ b/app/src/main/res/raw/licenses.xml
@@ -91,13 +91,6 @@
BSD 3-Clause License
-
- Apache Commons Net
- https://commons.apache.org/proper/commons-net/
- Copyright 2003 The Apache Software Foundation
- Apache Software License 2.0
-
-
Insetter
https://github.com/chrisbanes/insetter
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index a02400fda..f388ed947 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -376,7 +376,6 @@
无存储
添加存储
外部存储
- FTP 服务器
SFTP 服务器
SMB 服务器
编辑设备存储
@@ -387,34 +386,6 @@
名称
URI
路径
- 编辑 FTP 服务器
- 添加 FTP 服务器
- 主机名
- 输入主机名
- 无效主机名
- 端口
- 无效端口
- 路径
- 可留空
- 名称
- 使用主机名
- 协议
- 验证
-
- - 密码
- - 匿名
-
- 用户名
- 输入用户名
- 密码
- 模式
-
- - 主动
- - 被动
-
- 编码
- 连接并添加
- 添加
编辑 SFTP 服务器
添加 SFTP 服务器
主机名
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index b86af2be1..c7aa50edd 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -376,7 +376,6 @@
無儲存空間
新增儲存空間
外部儲存空間
- FTP 伺服器
SFTP 伺服器
SMB 伺服器
編輯裝置儲存空間
@@ -387,34 +386,6 @@
名稱
URI
路徑
- 編輯 FTP 伺服器
- 新增 FTP 伺服器
- 主機名稱
- 輸入主機名稱
- 無效的主機名稱
- 連接埠
- 無效的連接埠
- 路徑
- 可留空
- 名稱
- 使用主機名稱
- 通訊協定
- 驗證
-
- - 密碼
- - 匿名
-
- 使用者名稱
- 輸入使用者名稱
- 密碼
- 模式
-
- - 主動
- - 被動
-
- 編碼
- 連線並新增
- 新增
編輯 SFTP 伺服器
新增 SFTP 伺服器
主機名稱
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bc06c74b4..d77215ad8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -490,7 +490,6 @@
Android/data
Android/obb
External storage
- FTP server
SFTP server
SMB server
Edit device storage
@@ -501,40 +500,6 @@
Name
URI
Path
- Edit FTP server
- Add FTP server
- Hostname
- Enter a hostname
- Invalid hostname
- Port
- 21
- Invalid port
- Path
- Can be left empty
- Name
- Use hostname
- Protocol
-
- - FTP
- - FTPS
- - FTPES
-
- Authentication
-
- - Password
- - Anonymous
-
- Username
- Enter a username
- Password
- Mode
-
- - Active
- - Passive
-
- Encoding
- Connect and add
- Add
Edit SFTP server
Add SFTP server
Hostname