diff options
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.js | 85 |
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(); + } +} |