aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js85
1 files changed, 85 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js b/packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js
new file mode 100644
index 0000000..2093098
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js
@@ -0,0 +1,85 @@
+// Where the Android application's backups go (docs/MESHBAY_DESIGN.md §9.12):
+// one folder of one group the account owns and is the only member of, shared
+// by every kind of data. Each kind goes into its own folder under it,
+// `<folder>/<account>-<kind>`, named by the phone.
+//
+// What a group's folder holds every member can read; photos, an address book
+// or a conversation are not the group's. So the group is checked when it is
+// chosen and again at every run, before anything is read: somebody who joins
+// later stops the backups rather than reading them from then on.
+
+import { ConnectionPool } from './connection-pool.js';
+import * as platform from './platform.js';
+
+let ctx = null; // { hub, hubFetch, getUser, getBundleKey }
+
+/** Set by whichever runner starts first; both are handed the same context. */
+export function setSyncContext(context) { ctx = context; }
+
+export const destination = {
+ get: () => platform.phoneSync.destination(),
+ set: (d) => platform.phoneSync.setDestination(d),
+};
+
+/** True when this account owns the group, is its one member, and nobody is invited. */
+export async function isPrivate(groupId, user) {
+ const r = await ctx.hubFetch(`/v1/groups/${groupId}/members`, { token: user.token });
+ const members = r.members || [];
+ return members.length === 1 && members[0].user_id === user.userId
+ && r.admin_id === user.userId && !(r.invited || []).length;
+}
+
+/**
+ * Before a run reads anything: null when the destination may be written to,
+ * otherwise `not_private` or `not_a_member` (lasting) or `offline`.
+ */
+export async function checkDestination(dest, user) {
+ try {
+ return await isPrivate(dest.groupId, user) ? null : 'not_private';
+ } catch (e) {
+ return /not a member|not found/i.test(e.message || '') ? 'not_a_member' : 'offline';
+ }
+}
+
+/** The groups among `groups` this account owns and is alone in; a failed look leaves one out. */
+export async function privateGroups(groups) {
+ const user = ctx && ctx.getUser();
+ if (!user) return [];
+ const alone = await Promise.all(groups.map((g) => isPrivate(g.id, user).catch(() => false)));
+ return groups.filter((_, i) => alone[i]);
+}
+
+/** Every folder a member can add to: on a writable root whose drive is there. */
+export function writableFolders(index) {
+ const roots = new Set((index.roots || [])
+ .filter((r) => r.writable && r.available !== false).map((r) => r.name));
+ const all = new Set([...(index.dirs || []), ...roots]);
+ return [...all].filter((d) => roots.has(d.split('/')[0])).sort((a, b) => a.localeCompare(b));
+}
+
+/** True when the group's Photos tab shows `dir`. */
+export function photosTabShows(dir, photoDirectories) {
+ return (photoDirectories || []).some((p) => dir === p || dir.startsWith(`${p}/`));
+}
+
+/**
+ * A one-off look at a group, for choosing a folder: connected, read, closed.
+ * `photoDirectories` is what its Photos tab shows, for saying when photos sent
+ * there would show nowhere.
+ */
+export async function probeGroup(groupId) {
+ const user = ctx && ctx.getUser();
+ if (!user) throw new Error('Not signed in');
+ const pool = new ConnectionPool(ctx.hub);
+ try {
+ const bundleKey = await ctx.getBundleKey();
+ const conn = await pool.connect(groupId, user.token, bundleKey, user.username, user.userId);
+ const index = await conn.transport.fetchIndex();
+ return {
+ folders: writableFolders(index),
+ photoDirectories: (conn.ack && conn.ack.photo_directories) || [],
+ };
+ } finally {
+ pool.closeAll();
+ }
+}