more accurate sync progress report
This commit is contained in:
parent
8e3512b5cd
commit
c0f6e802ed
12 changed files with 390 additions and 63 deletions
|
|
@ -623,7 +623,7 @@ function writeStream(abs, stream) {
|
|||
try {
|
||||
if (st && st.isSymbolicLink()) removeAbs(abs)
|
||||
fs.renameSync(tmp, abs)
|
||||
resolve()
|
||||
resolve(out.bytesWritten || 0)
|
||||
} catch (err) {
|
||||
removeAbs(tmp)
|
||||
reject(err)
|
||||
|
|
@ -701,6 +701,34 @@ function createServerState() {
|
|||
return { tokens: new Map(), verifying: new Set(), session: null }
|
||||
}
|
||||
|
||||
// Which counter a sync unit feeds on the receiving side (the unit key looks
|
||||
// like "app:<id>", "wine:<name>" or "prefix:<name>").
|
||||
const UNIT_KINDS = { app: 'apps', wine: 'wines', prefix: 'prefixes' }
|
||||
|
||||
// Progress of a sync THIS device receives. The pushing device drives the
|
||||
// transfer, but the user here also needs to see what is being written to
|
||||
// their library: the launcher forwards this to its window, which shows a
|
||||
// "Receiving from X…" popup with a bar.
|
||||
function emitReceive(hooks, session, stage, extra = {}) {
|
||||
if (!hooks || !hooks.onReceive) return
|
||||
try {
|
||||
hooks.onReceive({
|
||||
stage,
|
||||
phase: stage,
|
||||
from: session.name || '',
|
||||
deviceId: session.deviceId || '',
|
||||
unit: extra.unit || '',
|
||||
label: extra.unit || '',
|
||||
done: session.received || 0,
|
||||
total: session.planned || 0,
|
||||
bytes: session.bytes || 0,
|
||||
totalBytes: session.plannedBytes || 0,
|
||||
items: { ...(session.items || {}) },
|
||||
deleted: extra.deleted || 0
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function activeSession(state) {
|
||||
if (!state.session) return null
|
||||
if (Date.now() - state.session.lastAt > SESSION_IDLE_MS) {
|
||||
|
|
@ -738,11 +766,11 @@ function createHttpServer(state, hooks) {
|
|||
if (route === '/plwl/verify' && req.method === 'POST') return handleVerify(state, req, res, hooks)
|
||||
const session = authorize(state, url.searchParams.get('token'))
|
||||
if (!session) return sendJson(res, 401, { ok: false, error: 'unauthorized' })
|
||||
if (route === '/plwl/unit/plan' && req.method === 'POST') return handlePlan(state, session, url, req, res)
|
||||
if (route === '/plwl/unit/plan' && req.method === 'POST') return handlePlan(state, session, url, req, res, hooks)
|
||||
if (route === '/plwl/unit/file' && req.method === 'PUT') return handlePut(session, url, req, res, hooks)
|
||||
if (route === '/plwl/unit/commit' && req.method === 'POST') return handleCommit(session, url, res)
|
||||
if (route === '/plwl/unit/commit' && req.method === 'POST') return handleCommit(session, url, res, hooks)
|
||||
if (route === '/plwl/apply' && req.method === 'POST') return handleApply(session, req, res, hooks)
|
||||
if (route === '/plwl/end' && req.method === 'POST') return handleEnd(state, session, res)
|
||||
if (route === '/plwl/end' && req.method === 'POST') return handleEnd(state, session, res, hooks)
|
||||
sendJson(res, 404, { ok: false, error: 'not-found' })
|
||||
} catch (err) {
|
||||
sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
||||
|
|
@ -777,10 +805,19 @@ async function handleVerify(state, req, res, hooks) {
|
|||
name: String(payload.name || ''),
|
||||
expires: Date.now() + TOKEN_TTL_MS,
|
||||
lastAt: Date.now(),
|
||||
units: new Map()
|
||||
units: new Map(),
|
||||
// counters behind the "Receiving from X…" popup: files written, bytes
|
||||
// written, and the same totals the sender planned (learned per unit)
|
||||
counted: new Set(),
|
||||
received: 0,
|
||||
planned: 0,
|
||||
bytes: 0,
|
||||
plannedBytes: 0,
|
||||
items: { apps: 0, wines: 0, prefixes: 0 }
|
||||
}
|
||||
state.tokens.set(token, session)
|
||||
state.session = session
|
||||
emitReceive(hooks, session, 'start')
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
token,
|
||||
|
|
@ -793,8 +830,9 @@ async function handleVerify(state, req, res, hooks) {
|
|||
}
|
||||
}
|
||||
|
||||
async function handlePlan(state, session, url, req, res) {
|
||||
async function handlePlan(state, session, url, req, res, hooks) {
|
||||
const key = url.searchParams.get('unit') || ''
|
||||
const label = url.searchParams.get('label') || ''
|
||||
let roots = []
|
||||
try {
|
||||
roots = JSON.parse(url.searchParams.get('roots') || '[]')
|
||||
|
|
@ -810,6 +848,7 @@ async function handlePlan(state, session, url, req, res) {
|
|||
const paths = new Set()
|
||||
const upload = []
|
||||
const same = []
|
||||
let uploadBytes = 0
|
||||
for (const entry of entries) {
|
||||
if (!withinRoots(entry.path, owned)) continue
|
||||
const abs = filesPath(entry.path)
|
||||
|
|
@ -822,10 +861,26 @@ async function handlePlan(state, session, url, req, res) {
|
|||
}
|
||||
if (entry.type === 'dir') ensureDir(abs)
|
||||
else if (entry.type === 'link') ensureLink(abs, entry.target)
|
||||
else upload.push(entry.path)
|
||||
else {
|
||||
upload.push(entry.path)
|
||||
uploadBytes += entry.size || 0
|
||||
}
|
||||
}
|
||||
session.units.set(key, { roots: owned, paths })
|
||||
// what this unit adds to the transfer: the popup reports files and bytes
|
||||
// against these as they arrive
|
||||
session.planned += upload.length
|
||||
session.plannedBytes += uploadBytes
|
||||
const kind = UNIT_KINDS[String(key).split(':')[0]]
|
||||
if (kind && !session.counted.has(key)) {
|
||||
session.counted.add(key)
|
||||
session.items[kind] = (session.items[kind] || 0) + 1
|
||||
}
|
||||
// the unit's human label (game / prefix / wine name) is announced while it
|
||||
// is planned, so the later file events can name it in the popup
|
||||
session.unitLabel = label
|
||||
sendJson(res, 200, { ok: true, upload, same, entries: paths.size })
|
||||
emitReceive(hooks, session, 'plan', { unit: label })
|
||||
}
|
||||
|
||||
async function handlePut(session, url, req, res, hooks) {
|
||||
|
|
@ -838,15 +893,17 @@ async function handlePut(session, url, req, res, hooks) {
|
|||
return sendJson(res, 400, { ok: false, error: 'bad-path' })
|
||||
}
|
||||
try {
|
||||
await writeStream(abs, req)
|
||||
const written = await writeStream(abs, req)
|
||||
session.received = (session.received || 0) + 1
|
||||
session.bytes = (session.bytes || 0) + (written || 0)
|
||||
sendJson(res, 200, { ok: true })
|
||||
emitReceive(hooks, session, 'copy', { unit: session.unitLabel || '' })
|
||||
} catch (err) {
|
||||
sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCommit(session, url, res) {
|
||||
async function handleCommit(session, url, res, hooks) {
|
||||
const key = url.searchParams.get('unit') || ''
|
||||
const unit = session.units.get(key)
|
||||
if (!unit) return sendJson(res, 400, { ok: false, error: 'no-unit' })
|
||||
|
|
@ -856,7 +913,9 @@ async function handleCommit(session, url, res) {
|
|||
if (abs) removed += await pruneTree(abs, root, unit.paths)
|
||||
}
|
||||
session.units.delete(key)
|
||||
session.deleted = (session.deleted || 0) + removed
|
||||
sendJson(res, 200, { ok: true, deleted: removed })
|
||||
emitReceive(hooks, session, 'commit', { unit: session.unitLabel || '', deleted: removed })
|
||||
}
|
||||
|
||||
async function handleApply(session, req, res, hooks) {
|
||||
|
|
@ -867,8 +926,17 @@ async function handleApply(session, req, res, hooks) {
|
|||
const winesDeleted = cats.has('wines') ? await applyWines(payload) : 0
|
||||
const prefixesDeleted = cats.has('prefixes') ? applyPrefixes(payload) : 0
|
||||
const removed = appsDeleted + winesDeleted + prefixesDeleted
|
||||
emitReceive(hooks, session, 'registry', { deleted: removed })
|
||||
if (hooks.onApplied) {
|
||||
hooks.onApplied({ from: session.name, deviceId: session.deviceId, categories: [...cats], files: session.received || 0, deleted: removed })
|
||||
hooks.onApplied({
|
||||
from: session.name,
|
||||
deviceId: session.deviceId,
|
||||
categories: [...cats],
|
||||
files: session.received || 0,
|
||||
bytes: session.bytes || 0,
|
||||
items: { ...(session.items || {}) },
|
||||
deleted: removed
|
||||
})
|
||||
}
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
|
|
@ -879,10 +947,11 @@ async function handleApply(session, req, res, hooks) {
|
|||
})
|
||||
}
|
||||
|
||||
function handleEnd(state, session, res) {
|
||||
function handleEnd(state, session, res, hooks) {
|
||||
state.tokens.delete(session.token)
|
||||
if (state.session && state.session.token === session.token) state.session = null
|
||||
sendJson(res, 200, { ok: true })
|
||||
emitReceive(hooks, session, 'done', { deleted: session.deleted || 0 })
|
||||
}
|
||||
|
||||
// --- discovery --------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue