1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
package org.meshbay.client.phonesync
import android.content.SharedPreferences
import org.json.JSONArray
import org.json.JSONObject
import org.meshbay.client.bridge.Refused
/**
* Where everything this phone backs up goes: one folder of one group the
* account owns and is the only member of (docs/MESHBAY_DESIGN.md §9.12).
* Each kind of data goes into its own folder under it, `<account>-<kind>`.
*/
data class Destination(
val account: String,
val groupId: String,
val groupName: String,
/** A folder among the group's own, as a virtual path (`Backups`). */
val folder: String,
) {
/** What was sent belongs to this, so a different group or folder starts again from nothing. */
val ledgerKey: String get() = "$account\n$groupId\n$folder"
fun toJson(): JSONObject = JSONObject()
.put("account", account).put("groupId", groupId).put("groupName", groupName).put("folder", folder)
companion object {
/** From the page, so checked like any input: names it chose, never a path on this phone. */
fun fromJson(o: JSONObject): Destination {
val account = o.optString("account", "").take(64)
val groupId = o.optString("groupId", "").take(64)
val folder = o.optString("folder", "").trim().trim('/').take(1024)
require(account.isNotEmpty() && groupId.isNotEmpty() && folder.isNotEmpty()) { "incomplete" }
require(DocPlan.SAFE_ACCOUNT.matches(account)) { "bad account" }
require(folder.split('/').none { it.isEmpty() || it == "." || it == ".." }) { "bad folder" }
return Destination(account, groupId, o.optString("groupName", "").take(256), folder)
}
fun parse(text: String?): Destination? = try {
val o = JSONObject(text ?: return null)
Destination(o.getString("account"), o.getString("groupId"), o.optString("groupName"), o.getString("folder"))
} catch (e: Exception) { null }
}
}
/**
* The destination, kept once for every kind. Changing it is a new backup for
* each of them: what they remember having sent belongs to the old place, so
* each is told and starts again from nothing.
*/
class DestinationChannels(private val prefs: SharedPreferences) {
private val listeners = mutableListOf<() -> Unit>()
fun onChange(listener: () -> Unit) { listeners += listener }
fun get(): Destination? = Destination.parse(prefs.getString(KEY, null))
fun handles(channel: String) = channel.startsWith("phonesync:")
fun call(channel: String, args: JSONArray): Any? = when (channel) {
"phonesync:destination" -> get()?.toJson() ?: JSONObject.NULL
"phonesync:set-destination" -> { set(args.optJSONObject(0)); get()?.toJson() ?: JSONObject.NULL }
else -> throw Refused("Refused: no such channel")
}
private fun set(o: JSONObject?) {
val previous = get()
val next = o?.let {
try { Destination.fromJson(it) } catch (e: IllegalArgumentException) { throw Refused("Refused: ${e.message}") }
}
if (next == null) prefs.edit().remove(KEY).apply()
else prefs.edit().putString(KEY, next.toJson().toString()).apply()
if (previous?.ledgerKey != next?.ledgerKey) listeners.forEach { it() }
}
companion object {
const val PREFS = "phonesync"
private const val KEY = "destination"
}
}
|