// 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, // `/-`, 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(); } }