Skip to content

Commit

Permalink
Add support for reporting filesystem usage per-remote
Browse files Browse the repository at this point in the history
This is disabled by default because some remotes are extremely slow at
computing the filesystem usage. Google Drive, in particular, sometimes
takes 30 seconds or more to return a result, even with a near empty
drive.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
  • Loading branch information
chenxiaolong committed Oct 30, 2024
1 parent d4656f7 commit e8a079a
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 60 deletions.
1 change: 1 addition & 0 deletions app/src/main/java/com/chiller3/rsaf/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Preferences(private val context: Context) {
const val PREF_ALLOW_EXTERNAL_ACCESS = "allow_external_access"
const val PREF_DYNAMIC_SHORTCUT = "dynamic_shortcut"
const val PREF_VFS_CACHING = "vfs_caching"
const val PREF_REPORT_USAGE = "report_usage"

// Not associated with a UI preference
const val PREF_DEBUG_MODE = "debug_mode"
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/com/chiller3/rsaf/rclone/RcloneProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,13 @@ class RcloneProvider : DocumentsProvider(), SharedPreferences.OnSharedPreference
continue
}

val usage = if (RcloneRpc.getCustomBoolOpt(config, RcloneRpc.CUSTOM_OPT_REPORT_USAGE)) {
debugLog("Querying filesystem usage: $remote")
RcloneRpc.getUsage("$remote:")
} else {
null
}

newRow().apply {
// Required
add(DocumentsContract.Root.COLUMN_ROOT_ID, remote)
Expand All @@ -434,6 +441,15 @@ class RcloneProvider : DocumentsProvider(), SharedPreferences.OnSharedPreference

// Optional
add(DocumentsContract.Root.COLUMN_SUMMARY, remote)

usage?.total?.let {
debugLog("Remote reports total space: $remote: $it")
add(DocumentsContract.Root.COLUMN_CAPACITY_BYTES, it)
}
usage?.free?.let {
debugLog("Remote reports free space: $remote: $it")
add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, it)
}
}
}
}
Expand Down
33 changes: 33 additions & 0 deletions app/src/main/java/com/chiller3/rsaf/rclone/RcloneRpc.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ object RcloneRpc {
const val CUSTOM_OPT_BLOCKED = CUSTOM_OPT_PREFIX + "hidden"
const val CUSTOM_OPT_DYNAMIC_SHORTCUT = CUSTOM_OPT_PREFIX + "dynamic_shortcut"
const val CUSTOM_OPT_VFS_CACHING = CUSTOM_OPT_PREFIX + "vfs_caching"
const val CUSTOM_OPT_REPORT_USAGE = CUSTOM_OPT_PREFIX + "report_usage"

private const val DEFAULT_BLOCKED = false
private const val DEFAULT_DYNAMIC_SHORTCUT = false
private const val DEFAULT_VFS_CACHING = true
private const val DEFAULT_REPORT_USAGE = false

/**
* Perform an rclone RPC call.
Expand Down Expand Up @@ -398,9 +400,40 @@ object RcloneRpc {
CUSTOM_OPT_BLOCKED -> DEFAULT_BLOCKED
CUSTOM_OPT_DYNAMIC_SHORTCUT -> DEFAULT_DYNAMIC_SHORTCUT
CUSTOM_OPT_VFS_CACHING -> DEFAULT_VFS_CACHING
CUSTOM_OPT_REPORT_USAGE -> DEFAULT_REPORT_USAGE
else -> throw IllegalArgumentException("Invalid custom option: $opt")
}

return config[opt]?.toBooleanStrictOrNull() ?: default
}

data class Usage(
val total: Long?,
val used: Long?,
val trashed: Long?,
val other: Long?,
val free: Long?,
val objects: Long?,
)

/** Get the filesystem usage. */
fun getUsage(remote: String): Usage {
val output = invoke("operations/about", JSONObject().put("fs", remote))

fun getOptLong(name: String) =
if (output.isNull(name)) {
null
} else {
output.getLong(name)
}

return Usage(
getOptLong("total"),
getOptLong("used"),
getOptLong("trashed"),
getOptLong("other"),
getOptLong("free"),
getOptLong("objects"),
)
}
}
10 changes: 5 additions & 5 deletions app/src/main/java/com/chiller3/rsaf/settings/EditRemoteAlert.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ sealed interface EditRemoteAlert {
val error: String,
) : EditRemoteAlert

data class UpdateExternalAccessFailed(val remote: String, val error: String) : EditRemoteAlert

data class UpdateDynamicShortcutFailed(val remote: String, val error: String) : EditRemoteAlert

data class UpdateVfsCachingFailed(val remote: String, val error: String) : EditRemoteAlert
data class SetConfigFailed(
val remote: String,
val opt: String,
val error: String,
) : EditRemoteAlert
}
31 changes: 23 additions & 8 deletions app/src/main/java/com/chiller3/rsaf/settings/EditRemoteFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
private lateinit var prefAllowExternalAccess: SwitchPreferenceCompat
private lateinit var prefDynamicShortcut: SwitchPreferenceCompat
private lateinit var prefVfsCaching: SwitchPreferenceCompat
private lateinit var prefReportUsage: SwitchPreferenceCompat

private lateinit var remote: String

Expand Down Expand Up @@ -89,6 +90,9 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
prefVfsCaching = findPreference(Preferences.PREF_VFS_CACHING)!!
prefVfsCaching.onPreferenceChangeListener = this

prefReportUsage = findPreference(Preferences.PREF_REPORT_USAGE)!!
prefReportUsage.onPreferenceChangeListener = this

remote = requireArguments().getString(ARG_REMOTE)!!
viewModel.setRemote(remote)

Expand Down Expand Up @@ -116,15 +120,27 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
prefDynamicShortcut.isChecked = it.dynamicShortcut
}

prefVfsCaching.isEnabled = it.allowExternalAccess ?: false && it.canStream ?: false
prefVfsCaching.isEnabled = it.allowExternalAccess ?: false
&& it.features?.putStream ?: false
if (it.vfsCaching != null) {
prefVfsCaching.isChecked = it.vfsCaching
}
prefVfsCaching.summary = when (it.canStream) {
prefVfsCaching.summary = when (it.features?.putStream) {
null -> getString(R.string.pref_edit_remote_vfs_caching_desc_loading)
true -> getString(R.string.pref_edit_remote_vfs_caching_desc_optional)
false -> getString(R.string.pref_edit_remote_vfs_caching_desc_required)
}

prefReportUsage.isEnabled = it.allowExternalAccess ?: false
&& it.features?.about ?: false
if (it.reportUsage != null) {
prefReportUsage.isChecked = it.reportUsage
}
prefReportUsage.summary = when (it.features?.about) {
null -> getString(R.string.pref_edit_remote_report_usage_desc_loading)
true -> getString(R.string.pref_edit_remote_report_usage_desc_supported)
false -> getString(R.string.pref_edit_remote_report_usage_desc_unsupported)
}
}
}
}
Expand Down Expand Up @@ -247,6 +263,9 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
prefVfsCaching -> {
viewModel.setVfsCaching(remote, newValue as Boolean)
}
prefReportUsage -> {
viewModel.setReportUsage(remote, newValue as Boolean)
}
}

return false
Expand All @@ -266,12 +285,8 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
is EditRemoteAlert.RemoteDuplicateFailed ->
getString(R.string.alert_duplicate_remote_failure, alert.oldRemote, alert.newRemote,
alert.error)
is EditRemoteAlert.UpdateExternalAccessFailed ->
getString(R.string.alert_update_external_access_failure, alert.remote, alert.error)
is EditRemoteAlert.UpdateDynamicShortcutFailed ->
getString(R.string.alert_update_dynamic_shortcut_failure, alert.remote, alert.error)
is EditRemoteAlert.UpdateVfsCachingFailed ->
getString(R.string.alert_update_vfs_caching_failure, alert.remote, alert.error)
is EditRemoteAlert.SetConfigFailed ->
getString(R.string.alert_set_config_failure, alert.opt, alert.remote, alert.error)
}

Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG)
Expand Down
73 changes: 32 additions & 41 deletions app/src/main/java/com/chiller3/rsaf/settings/EditRemoteViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package com.chiller3.rsaf.settings
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.chiller3.rsaf.binding.rcbridge.RbRemoteFeaturesResult
import com.chiller3.rsaf.binding.rcbridge.Rcbridge
import com.chiller3.rsaf.rclone.RcloneConfig
import com.chiller3.rsaf.rclone.RcloneRpc
Expand All @@ -28,7 +29,8 @@ data class RemoteConfigState(
val allowExternalAccess: Boolean? = null,
val dynamicShortcut: Boolean? = null,
val vfsCaching: Boolean? = null,
val canStream: Boolean? = null,
val reportUsage: Boolean? = null,
val features: RbRemoteFeaturesResult? = null,
)

class EditRemoteViewModel : ViewModel() {
Expand Down Expand Up @@ -80,15 +82,19 @@ class EditRemoteViewModel : ViewModel() {
config,
RcloneRpc.CUSTOM_OPT_VFS_CACHING,
),
reportUsage = RcloneRpc.getCustomBoolOpt(
config,
RcloneRpc.CUSTOM_OPT_REPORT_USAGE,
),
)
}

// Only calculate this once since the value can't change and it requires
// initializing the backend, which may perform network calls.
if (_remoteConfig.value.canStream == null) {
if (_remoteConfig.value.features == null) {
withContext(Dispatchers.IO) {
_remoteConfig.update {
it.copy(canStream = Rcbridge.rbCanStream("$remote:"))
it.copy(features = Rcbridge.rbRemoteFeatures("$remote:"))
}
}
}
Expand All @@ -108,58 +114,43 @@ class EditRemoteViewModel : ViewModel() {
}
}

fun setExternalAccess(remote: String, allow: Boolean) {
private fun setCustomOpt(
remote: String,
opt: String,
value: Boolean,
onSuccess: (() -> Unit)? = null,
) {
viewModelScope.launch {
try {
withContext(Dispatchers.IO) {
RcloneRpc.setRemoteOptions(
remote, mapOf(
RcloneRpc.CUSTOM_OPT_BLOCKED to (!allow).toString(),
)
)
RcloneRpc.setRemoteOptions(remote, mapOf(opt to value.toString()))
}
refreshRemotesInternal(true)
_activityActions.update { it.copy(refreshRoots = true) }
onSuccess?.let { it() }
} catch (e: Exception) {
Log.w(TAG, "Failed to set $remote external access to $allow", e)
_alerts.update { it + EditRemoteAlert.UpdateExternalAccessFailed(remote, e.toString()) }
Log.w(TAG, "Failed to set $remote config option $opt to $value", e)
_alerts.update { it + EditRemoteAlert.SetConfigFailed(remote, opt, e.toString()) }
}
}
}

fun setDynamicShortcut(remote: String, enabled: Boolean) {
viewModelScope.launch {
try {
withContext(Dispatchers.IO) {
RcloneRpc.setRemoteOptions(
remote, mapOf(
RcloneRpc.CUSTOM_OPT_DYNAMIC_SHORTCUT to enabled.toString(),
)
)
}
refreshRemotesInternal(true)
} catch (e: Exception) {
Log.w(TAG, "Failed to set remote $remote shortcut state to $enabled", e)
_alerts.update { it + EditRemoteAlert.UpdateDynamicShortcutFailed(remote, e.toString()) }
}
fun setExternalAccess(remote: String, allow: Boolean) {
setCustomOpt(remote, RcloneRpc.CUSTOM_OPT_BLOCKED, !allow) {
_activityActions.update { it.copy(refreshRoots = true) }
}
}

fun setDynamicShortcut(remote: String, enabled: Boolean) {
setCustomOpt(remote, RcloneRpc.CUSTOM_OPT_DYNAMIC_SHORTCUT, enabled)
}

fun setVfsCaching(remote: String, enabled: Boolean) {
viewModelScope.launch {
try {
withContext(Dispatchers.IO) {
RcloneRpc.setRemoteOptions(
remote, mapOf(
RcloneRpc.CUSTOM_OPT_VFS_CACHING to enabled.toString(),
)
)
}
refreshRemotesInternal(true)
} catch (e: Exception) {
Log.w(TAG, "Failed to set remote $remote VFS caching state to $enabled", e)
_alerts.update { it + EditRemoteAlert.UpdateVfsCachingFailed(remote, e.toString()) }
}
setCustomOpt(remote, RcloneRpc.CUSTOM_OPT_VFS_CACHING, enabled)
}

fun setReportUsage(remote: String, enabled: Boolean) {
setCustomOpt(remote, RcloneRpc.CUSTOM_OPT_REPORT_USAGE, enabled) {
_activityActions.update { it.copy(refreshRoots = true) }
}
}

Expand Down
8 changes: 5 additions & 3 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
<string name="pref_edit_remote_vfs_caching_desc_loading">(Checking if streaming is supported…)</string>
<string name="pref_edit_remote_vfs_caching_desc_optional">VFS caching enables support for random writes and allows failed uploads to be retried. However, files do not begin uploading until the client app closes them.</string>
<string name="pref_edit_remote_vfs_caching_desc_required">VFS caching cannot be disabled because this remote type does not support streaming uploads.</string>
<string name="pref_edit_remote_report_usage_name">Report filesystem usage</string>
<string name="pref_edit_remote_report_usage_desc_loading">(Checking if filesystem usage reporting is supported…)</string>
<string name="pref_edit_remote_report_usage_desc_supported">Report free space and total space to client apps. For some remote types, this can significantly slow down client apps when they fetch the list of remotes.</string>
<string name="pref_edit_remote_report_usage_desc_unsupported">This remote type does not support reporting its free space and total space.</string>

<!-- Main alerts -->
<string name="alert_list_remotes_failure">Failed to get list of remotes: %1$s</string>
Expand All @@ -84,9 +88,7 @@
<string name="alert_delete_remote_failure">Failed to delete %1$s: %2$s</string>
<string name="alert_rename_remote_failure">Failed to rename remote %1$s to %2$s: %3$s</string>
<string name="alert_duplicate_remote_failure">Failed to duplicate remote %1$s to %2$s: %3$s</string>
<string name="alert_update_external_access_failure">Failed to update external app access to remote %1$s: %2$s</string>
<string name="alert_update_dynamic_shortcut_failure">Failed to update launcher shortcut for remote %1$s: %2$s</string>
<string name="alert_update_vfs_caching_failure">Failed to update VFS caching for remote %1$s: %2$s</string>
<string name="alert_set_config_failure">Failed to set %1$s config option for remote %2$s: %3$s</string>

<!-- Biometric -->
<string name="biometric_title">Unlock configuration</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/xml/preferences_edit_remote.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,11 @@
app:title="@string/pref_edit_remote_vfs_caching_name"
app:iconSpaceReserved="false"
app:defaultValue="true" />

<SwitchPreferenceCompat
app:key="report_usage"
app:persistent="false"
app:title="@string/pref_edit_remote_report_usage_name"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</PreferenceScreen>
18 changes: 15 additions & 3 deletions rcbridge/rcbridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,14 +438,26 @@ func getVfsForDoc(doc string) (*vfs.VFS, string, error) {
return v, path, nil
}

type RbRemoteFeaturesResult struct {
PutStream bool
About bool
}

// Return whether the specified remote supports streaming.
func RbCanStream(remote string) (bool, error) {
func RbRemoteFeatures(remote string) (*RbRemoteFeaturesResult, error) {
f, err := getFs(remote)
if err != nil {
return false, err
return nil, err
}

features := f.Features()

result := RbRemoteFeaturesResult{
PutStream: features.PutStream != nil,
About: features.About != nil,
}

return f.Features().PutStream != nil, nil
return &result, nil
}

type RbRemoteSplitResult struct {
Expand Down

0 comments on commit e8a079a

Please sign in to comment.