Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for reporting filesystem usage per-remote #99

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading