cleanup & sync summary
This commit is contained in:
parent
cd0007fc7d
commit
be4f67fb60
17 changed files with 637 additions and 207 deletions
|
|
@ -166,11 +166,14 @@ function createPusher(api) {
|
|||
|
||||
// --- units ----------------------------------------------------------------
|
||||
|
||||
function appUnits(cats) {
|
||||
// `selected` is the optional app selection (a Set of ids or null = all);
|
||||
// the seam a future "sync only these apps" UI builds on.
|
||||
function appUnits(cats, selected) {
|
||||
const units = []
|
||||
for (const game of library.loadGames()) {
|
||||
const category = api.appCategory(game)
|
||||
if (!category || !cats.has(category)) continue
|
||||
if (selected && !selected.has(String(game.id))) continue
|
||||
const roots = [`data/icons/${game.id}`]
|
||||
if (category === 'apps-portable') roots.unshift(`apps/${game.id}`)
|
||||
units.push({ key: `app:${game.id}`, kind: 'app', id: game.id, label: game.name, roots })
|
||||
|
|
@ -178,6 +181,10 @@ function createPusher(api) {
|
|||
return units
|
||||
}
|
||||
|
||||
function selectedApps(selection) {
|
||||
return selection && Array.isArray(selection.apps) ? new Set(selection.apps.map(String)) : null
|
||||
}
|
||||
|
||||
function wineUnits() {
|
||||
const units = new Map()
|
||||
const add = (root, label) => {
|
||||
|
|
@ -200,9 +207,10 @@ function createPusher(api) {
|
|||
return [...names].map(name => ({ key: `prefix:${name}`, kind: 'prefix', id: name, label: name, roots: [`wineprefixes/${name}`] }))
|
||||
}
|
||||
|
||||
function buildUnits(cats) {
|
||||
function buildUnits(cats, selection) {
|
||||
const units = []
|
||||
if (cats.has('apps-portable') || cats.has('apps-prefix')) units.push(...appUnits(cats))
|
||||
const apps = selectedApps(selection)
|
||||
if (cats.has('apps-portable') || cats.has('apps-prefix')) units.push(...appUnits(cats, apps))
|
||||
if (cats.has('wines')) units.push(...wineUnits())
|
||||
if (cats.has('prefixes')) units.push(...prefixUnits())
|
||||
return units
|
||||
|
|
@ -215,17 +223,22 @@ function createPusher(api) {
|
|||
return library.portableGame(game)
|
||||
}
|
||||
|
||||
function buildRegistryPayload(categories) {
|
||||
function buildRegistryPayload(categories, selection) {
|
||||
const cats = new Set(categories)
|
||||
const payload = { categories: [...cats] }
|
||||
const selected = selectedApps(selection)
|
||||
if (cats.has('apps-portable') || cats.has('apps-prefix')) {
|
||||
payload.apps = library
|
||||
.loadGames()
|
||||
.filter(game => {
|
||||
const category = api.appCategory(game)
|
||||
return category ? cats.has(category) : false
|
||||
if (!category || !cats.has(category)) return false
|
||||
return !selected || selected.has(String(game.id))
|
||||
})
|
||||
.map(portableApp)
|
||||
// A partial list means "only these apps travel"; without the marker the
|
||||
// receiver mirrors the category and drops the apps that were left out.
|
||||
if (selected) payload.appScope = 'selected'
|
||||
}
|
||||
if (cats.has('wines')) {
|
||||
// the receiver prunes its own builds down to this list and rescans
|
||||
|
|
@ -282,6 +295,8 @@ function createPusher(api) {
|
|||
skipped: 0,
|
||||
deleted: 0,
|
||||
bytes: 0,
|
||||
// size of the synced trees as they now sit on the other device
|
||||
sizeBytes: 0,
|
||||
// the popup reports "3 games, 1 prefix" instead of file counts
|
||||
items: { apps: 0, wines: 0, prefixes: 0 }
|
||||
}
|
||||
|
|
@ -293,14 +308,14 @@ function createPusher(api) {
|
|||
}
|
||||
|
||||
// Push to the peer; a rejected code comes back as 'bad-code'.
|
||||
async function syncToDevice({ deviceId, device: target, code, categories, force, onProgress }) {
|
||||
async function syncToDevice({ deviceId, device: target, code, categories, force, selection, onProgress }) {
|
||||
const fail = (error, summary) => syncResult(false, error, summary || newSummary(0))
|
||||
const device = target || api.listDevices().find(d => d.id === deviceId)
|
||||
if (!device || !device.address || !device.port) return fail('offline')
|
||||
const cats = new Set(normalizeCategories(categories))
|
||||
if (!cats.size) return fail('nothing-selected')
|
||||
// units are known before anything is read: announce them with the verify
|
||||
const units = buildUnits(cats)
|
||||
const units = buildUnits(cats, selection)
|
||||
const bucketOf = kind => (kind === 'wine' ? 'wines' : kind === 'prefix' ? 'prefixes' : 'apps')
|
||||
const totals = { apps: 0, wines: 0, prefixes: 0, settings: 0 }
|
||||
for (const unit of units) totals[bucketOf(unit.kind)] += 1
|
||||
|
|
@ -420,6 +435,11 @@ function createPusher(api) {
|
|||
// for a receiver that still has to hash).
|
||||
const entries = []
|
||||
let writes = Promise.resolve()
|
||||
// A read failure means this manifest is not the sender's whole tree:
|
||||
// tell the receiver, so it never prunes what only looked missing.
|
||||
const incomplete = () => {
|
||||
writes = writes.then(() => write({ unitIncomplete: true }))
|
||||
}
|
||||
const plan = await planUnit(device, token, { ...unit, roots }, !!force, { totals }, async write => {
|
||||
await buildManifestStream(roots, !!force, {
|
||||
// the receiver sizes its compare bar from this, before any hashing
|
||||
|
|
@ -430,6 +450,7 @@ function createPusher(api) {
|
|||
entries.push(entry)
|
||||
writes = writes.then(() => write(entry))
|
||||
},
|
||||
onIncomplete: incomplete,
|
||||
onProgress: (hashedDone, hashedTotal, hashedBytes, totalBytes) => {
|
||||
barBytes = hashedBytes
|
||||
barTotal = totalBytes
|
||||
|
|
@ -444,6 +465,8 @@ function createPusher(api) {
|
|||
})
|
||||
if (abort.dead) throw new Error('receiver-lost')
|
||||
if (!plan.data || !plan.data.ok) throw new Error((plan.data && plan.data.error) || `http-${plan.status}`)
|
||||
// every file in the unit, including the ones already identical there
|
||||
for (const entry of entries) if (entry.type === 'file') summary.sizeBytes += entry.size || 0
|
||||
const upload = new Set(plan.data.upload || [])
|
||||
const same = new Set(plan.data.same || [])
|
||||
const filePaths = new Set(entries.filter(e => e.type === 'file').map(e => e.path))
|
||||
|
|
@ -485,13 +508,20 @@ function createPusher(api) {
|
|||
if (typeof api.mapLimit === 'function') await api.mapLimit(pending, UPLOAD_CONCURRENCY, send)
|
||||
else for (const entry of pending) await send(entry)
|
||||
if (failed) throw failed
|
||||
// A dead receiver made `send` skip files; committing now would prune
|
||||
// exactly the files that never arrived (the commit itself could even
|
||||
// succeed while only the heartbeats were failing).
|
||||
if (abort.dead) throw new Error('receiver-lost')
|
||||
}
|
||||
emit('copy', unit, allDone + pending.length, allTotal + pending.length, true)
|
||||
// the totals count a unit's files only once they are on the wire
|
||||
allDone += pending.length
|
||||
allTotal += pending.length
|
||||
const commit = await httpJson(device, 'POST', `/plwl/unit/commit?token=${encodeURIComponent(token)}&unit=${encodeURIComponent(unit.key)}`, { json: {} })
|
||||
if (commit.data && commit.data.deleted) summary.deleted += commit.data.deleted
|
||||
// a unit that did not commit (unknown unit, failed prune) must not be
|
||||
// counted as done while the sync moves on to the registry
|
||||
if (!commit.data || !commit.data.ok) throw new Error((commit.data && commit.data.error) || `commit-${commit.status}`)
|
||||
if (commit.data.deleted) summary.deleted += commit.data.deleted
|
||||
finished[kind] += 1
|
||||
// a finished unit leaves a finished bar behind, even one that sent none
|
||||
barTotal = barTotal || 1
|
||||
|
|
@ -499,7 +529,8 @@ function createPusher(api) {
|
|||
emit('commit', unit, null, null, true)
|
||||
}
|
||||
emit('registry', null, null, null, true)
|
||||
const applied = await httpJson(device, 'POST', `/plwl/apply?token=${encodeURIComponent(token)}`, { json: buildRegistryPayload([...cats]), timeout: 60000 })
|
||||
if (abort.dead) throw new Error('receiver-lost')
|
||||
const applied = await httpJson(device, 'POST', `/plwl/apply?token=${encodeURIComponent(token)}`, { json: buildRegistryPayload([...cats], selection), timeout: 60000 })
|
||||
if (!applied.data || !applied.data.ok) throw new Error((applied.data && applied.data.error) || `http-${applied.status}`)
|
||||
if (applied.data.deleted) summary.deleted += applied.data.deleted
|
||||
// a sync with nothing to send still ends with a full bar
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue