Skip to content

Commit

Permalink
Feat!: Add WebDAV support
Browse files Browse the repository at this point in the history
Bug: #191
  • Loading branch information
zhanghai committed Feb 27, 2024
1 parent e0a0361 commit 4af8957
Show file tree
Hide file tree
Showing 29 changed files with 1,970 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ An open source Material Design file manager, for Android 5.0+.
- Breadcrumbs: Navigate in the filesystem with ease.
- Root support: View and manage files with root access.
- Archive support: View, extract and create common compressed files.
- NAS support: View and manage files on FTP, SFTP and SMB servers.
- NAS support: View and manage files on FTP, SFTP, SMB and WebDAV servers.
- Themes: Customizable UI colors, plus night mode with optional true black.
- Linux-aware: Like [Nautilus](https://wiki.gnome.org/action/show/Apps/Files), knows symbolic links, file permissions and SELinux context.
- Robust: Uses Linux system calls under the hood, not yet another [`ls` parser](https://news.ycombinator.com/item?id=7994720).
Expand Down
2 changes: 1 addition & 1 deletion README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
- 面包屑导航栏:点击导航栏所显示路径中的任一文件夹即可快速访问。
- Root 支持:使用 root 权限查看和管理文件。
- 压缩文件支持:查看、提取和创建常见的压缩文件。
- NAS 支持:查看和管理 FTP、SFTP 和 SMB 服务器上的文件。
- NAS 支持:查看和管理 FTP、SFTP、SMBWebDAV 服务器上的文件。
- 主题:可定制的界面颜色,以及可选纯黑的夜间模式。
- Linux 友好:类似 [Nautilus](https://wiki.gnome.org/action/show/Apps/Files),支持符号链接、文件权限和 SELinux 上下文。
- 健壮性:使用 Linux 系统调用实现,而不是另一个 [`ls` 解析器](https://news.ycombinator.com/item?id=7994720)
Expand Down
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ repositories {
}
}
dependencies {
// TODO: Remove DavResource.moveCompat once https://github.com/bitfireAT/dav4jvm/issues/39 is
// fixed.
implementation 'com.github.bitfireAT:dav4jvm:2.2.1'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
releaseImplementation 'com.github.mypplication:stetho-noop:1.1'
implementation 'com.github.topjohnwu.libsu:service:5.2.2'
Expand Down
28 changes: 28 additions & 0 deletions app/src/main/java/at/bitfire/dav4jvm/DavResourceAccessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/

package at.bitfire.dav4jvm;

import java.io.IOException;

import androidx.annotation.NonNull;
import at.bitfire.dav4jvm.exception.DavException;
import at.bitfire.dav4jvm.exception.HttpException;
import kotlin.jvm.functions.Function0;
import okhttp3.Response;

public class DavResourceAccessor {
private DavResourceAccessor() {}

public static void checkStatus(@NonNull DavResource davResource, @NonNull Response response)
throws HttpException {
davResource.checkStatus(response);
}

public static Response followRedirects(@NonNull DavResource davResource,
@NonNull Function0<Response> sendRequest) throws DavException, IOException {
return davResource.followRedirects$build(sendRequest);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import me.zhanghai.android.files.compat.getSystemServiceCompat
import me.zhanghai.android.files.compat.mainExecutorCompat
import okhttp3.OkHttpClient
import java.util.concurrent.Executor

val appClassLoader = AppProvider::class.java.classLoader
Expand All @@ -31,6 +32,8 @@ val defaultSharedPreferences: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(application)
}

val okHttpClient: OkHttpClient by lazy { OkHttpClient() }

val inputMethodManager: InputMethodManager by lazy {
application.getSystemServiceCompat(InputMethodManager::class.java)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ package me.zhanghai.android.files.compat
import org.threeten.bp.DateTimeUtils
import org.threeten.bp.Instant
import java.util.Calendar
import java.util.Date

fun Calendar.toInstantCompat(): Instant = DateTimeUtils.toInstant(this)

fun Date.toInstantCompat(): Instant = DateTimeUtils.toInstant(this)

fun Instant.toDateCompat(): Date = DateTimeUtils.toDate(this)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import me.zhanghai.android.files.provider.linux.LinuxFileSystemProvider
import me.zhanghai.android.files.provider.root.isRunningAsRoot
import me.zhanghai.android.files.provider.sftp.SftpFileSystemProvider
import me.zhanghai.android.files.provider.smb.SmbFileSystemProvider
import me.zhanghai.android.files.provider.webdav.WebDavFileSystemProvider
import me.zhanghai.android.files.provider.webdav.WebDavsFileSystemProvider

object FileSystemProviders {
/**
Expand All @@ -42,6 +44,8 @@ object FileSystemProviders {
FileSystemProvider.installProvider(FtpesFileSystemProvider)
FileSystemProvider.installProvider(SftpFileSystemProvider)
FileSystemProvider.installProvider(SmbFileSystemProvider)
FileSystemProvider.installProvider(WebDavFileSystemProvider)
FileSystemProvider.installProvider(WebDavsFileSystemProvider)
}
Files.installFileTypeDetector(AndroidFileTypeDetector)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package me.zhanghai.android.files.provider.webdav

import at.bitfire.dav4jvm.exception.ConflictException
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.ForbiddenException
import at.bitfire.dav4jvm.exception.NotFoundException
import at.bitfire.dav4jvm.exception.UnauthorizedException
import java8.nio.file.AccessDeniedException
import java8.nio.file.FileAlreadyExistsException
import java8.nio.file.FileSystemException
import java8.nio.file.NoSuchFileException
import me.zhanghai.android.files.provider.webdav.client.DavIOException

fun DavException.toFileSystemException(
file: String?,
other: String? = null
): FileSystemException {
return when (this) {
is DavIOException ->
return FileSystemException(file, other, message).apply { initCause(cause) }
is UnauthorizedException, is ForbiddenException ->
AccessDeniedException(file, other, message)
is NotFoundException -> NoSuchFileException(file, other, message)
is ConflictException -> FileAlreadyExistsException(file, other, message)
else -> FileSystemException(file, other, message)
}.apply { initCause(this@toFileSystemException) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/

package me.zhanghai.android.files.provider.webdav

import java8.nio.file.StandardOpenOption
import me.zhanghai.android.files.provider.common.OpenOptions

internal fun OpenOptions.checkForWebDav() {
if (deleteOnClose) {
throw UnsupportedOperationException(StandardOpenOption.DELETE_ON_CLOSE.toString())
}
if (sync) {
throw UnsupportedOperationException(StandardOpenOption.SYNC.toString())
}
if (dsync) {
throw UnsupportedOperationException(StandardOpenOption.DSYNC.toString())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/

package me.zhanghai.android.files.provider.webdav

import java8.nio.file.Path
import me.zhanghai.android.files.provider.webdav.client.Authority

fun Authority.createWebDavRootPath(): Path =
WebDavFileSystemProvider.getOrNewFileSystem(this).rootDirectory
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/

package me.zhanghai.android.files.provider.webdav

import at.bitfire.dav4jvm.exception.DavException
import java8.nio.file.FileAlreadyExistsException
import java8.nio.file.NoSuchFileException
import java8.nio.file.StandardCopyOption
import me.zhanghai.android.files.provider.common.CopyOptions
import me.zhanghai.android.files.provider.common.copyTo
import me.zhanghai.android.files.provider.webdav.client.Client
import me.zhanghai.android.files.provider.webdav.client.isDirectory
import me.zhanghai.android.files.provider.webdav.client.isSymbolicLink
import me.zhanghai.android.files.provider.webdav.client.lastModifiedTime
import me.zhanghai.android.files.provider.webdav.client.size
import java.io.IOException

internal object WebDavCopyMove {
@Throws(IOException::class)
fun copy(source: WebDavPath, target: WebDavPath, copyOptions: CopyOptions) {
if (copyOptions.atomicMove) {
throw UnsupportedOperationException(StandardCopyOption.ATOMIC_MOVE.toString())
}
val sourceResponse = try {
Client.findProperties(source, copyOptions.noFollowLinks)
} catch (e: DavException) {
throw e.toFileSystemException(source.toString())
}
val targetFile = try {
Client.findPropertiesOrNull(target, true)
} catch (e: DavException) {
throw e.toFileSystemException(target.toString())
}
val sourceSize = sourceResponse.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)
} catch (e: DavException) {
throw e.toFileSystemException(target.toString())
}
}
when {
sourceResponse.isDirectory -> {
try {
Client.makeCollection(target)
} catch (e: DavException) {
throw e.toFileSystemException(target.toString())
}
copyOptions.progressListener?.invoke(sourceSize)
}
sourceResponse.isSymbolicLink ->
throw UnsupportedOperationException("Cannot copy symbolic links")
else -> {
val sourceInputStream = try {
Client.get(source)
} catch (e: DavException) {
throw e.toFileSystemException(source.toString())
}
try {
val targetOutputStream = try {
Client.put(target)
} catch (e: DavException) {
throw e.toFileSystemException(target.toString())
}
var successful = false
try {
sourceInputStream.copyTo(
targetOutputStream, copyOptions.progressIntervalMillis,
copyOptions.progressListener
)
successful = true
} finally {
try {
targetOutputStream.close()
} catch (e: DavException) {
throw e.toFileSystemException(target.toString())
} finally {
if (!successful) {
try {
Client.delete(target)
} catch (e: DavException) {
e.printStackTrace()
}
}
}
}
} finally {
try {
sourceInputStream.close()
} catch (e: DavException) {
throw e.toFileSystemException(source.toString())
}
}
}
}
// We don't take error when copying attribute fatal, so errors will only be logged from now
// on.
if (!sourceResponse.isSymbolicLink) {
val lastModifiedTime = sourceResponse.lastModifiedTime
if (lastModifiedTime != null) {
try {
Client.setLastModifiedTime(target, lastModifiedTime)
} catch (e: DavException) {
e.printStackTrace()
}
}
}
}

@Throws(IOException::class)
fun move(source: WebDavPath, target: WebDavPath, copyOptions: CopyOptions) {
val sourceResponse = try {
Client.findProperties(source, copyOptions.noFollowLinks)
} catch (e: DavException) {
throw e.toFileSystemException(source.toString())
}
val targetResponse = try {
Client.findPropertiesOrNull(target, true)
} catch (e: DavException) {
throw e.toFileSystemException(target.toString())
}
val sourceSize = sourceResponse.size
if (targetResponse != null) {
if (source == target) {
copyOptions.progressListener?.invoke(sourceSize)
return
}
if (!copyOptions.replaceExisting) {
throw FileAlreadyExistsException(source.toString(), target.toString(), null)
}
try {
Client.delete(target)
} catch (e: DavException) {
throw e.toFileSystemException(target.toString())
}
}
var renameSuccessful = false
try {
Client.move(source, target)
renameSuccessful = true
} catch (e: DavException) {
if (copyOptions.atomicMove) {
throw e.toFileSystemException(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)
} catch (e: DavException) {
if (e.toFileSystemException(source.toString()) !is NoSuchFileException) {
try {
Client.delete(target)
} catch (e2: DavException) {
e.addSuppressed(e2.toFileSystemException(target.toString()))
}
}
throw e.toFileSystemException(source.toString())
}
}
}
Loading

0 comments on commit 4af8957

Please sign in to comment.