code refactor & sync bugfixes
This commit is contained in:
parent
2bcb47076a
commit
9edcc9841b
20 changed files with 820 additions and 812 deletions
152
src/lansync-manifest.js
Normal file
152
src/lansync-manifest.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
'use strict'
|
||||
|
||||
// Sender-side hashing: walking sync units into a manifest of dirs, links and
|
||||
// files, with the digest of every file. Exported as a factory over the path
|
||||
// guards and the hash store, so every launcher state (and every test device)
|
||||
// builds manifests for its own files.
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const fsp = fs.promises
|
||||
|
||||
function createManifestBuilder({ filesPath, filesRoot, hash, mapLimit, concurrency }) {
|
||||
// Symlink targets travel RELATIVE whenever they point inside the launcher
|
||||
// data (a prefix linking into its wine build, an app linking to its own
|
||||
// assets): the mirrored trees have the same internal layout on both
|
||||
// machines, so a relative target is correct over there while an absolute one
|
||||
// would point at the sender's own folders - which is exactly how a synced
|
||||
// prefix ends up with hundreds of dangling links. Targets outside files/
|
||||
// (a system font, a mounted drive, /) are machine-specific and stay as they
|
||||
// are.
|
||||
function portableLinkTarget(linkAbs, target) {
|
||||
if (typeof target !== 'string' || !target || !path.isAbsolute(target)) return target
|
||||
const rel = path.relative(path.dirname(linkAbs), target)
|
||||
if (!rel || path.isAbsolute(rel)) return target
|
||||
const root = path.resolve(filesRoot)
|
||||
const resolved = path.resolve(path.dirname(linkAbs), rel)
|
||||
if (resolved !== root && !resolved.startsWith(root + path.sep)) return target
|
||||
return rel
|
||||
}
|
||||
|
||||
// Walk without following symlinks: builds and prefixes contain links that
|
||||
// are part of what is being synced (dosdevices, build internals), and
|
||||
// following them would both loop and copy the wrong thing.
|
||||
//
|
||||
// Iterative (a queue instead of an await-per-directory recursion) and with
|
||||
// the stat/readlink calls of one directory in flight together: walking a
|
||||
// prefix is dominated by those two syscalls, and serializing them costs more
|
||||
// than the readdir itself. Regular files are re-verified with one stat
|
||||
// (size, mode, mtime feed both the manifest and the hash cache), everything
|
||||
// else is skipped.
|
||||
async function scanTree(abs, rel, out, mtimes) {
|
||||
const queue = [[abs, rel]]
|
||||
while (queue.length) {
|
||||
const [dirAbs, dirRel] = queue.pop()
|
||||
let entries
|
||||
try {
|
||||
entries = await fsp.readdir(dirAbs, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
const dirs = []
|
||||
const leaves = []
|
||||
for (const entry of entries) {
|
||||
const relChild = `${dirRel}/${entry.name}`
|
||||
if (entry.isDirectory()) {
|
||||
out.push({ path: relChild, type: 'dir' })
|
||||
dirs.push([path.join(dirAbs, entry.name), relChild])
|
||||
} else {
|
||||
leaves.push(entry)
|
||||
}
|
||||
}
|
||||
await mapLimit(leaves, concurrency, async entry => {
|
||||
const absChild = path.join(dirAbs, entry.name)
|
||||
const relChild = `${dirRel}/${entry.name}`
|
||||
if (entry.isSymbolicLink()) {
|
||||
let target = ''
|
||||
try {
|
||||
target = await fsp.readlink(absChild)
|
||||
} catch {}
|
||||
out.push({ path: relChild, type: 'link', target: portableLinkTarget(absChild, target) })
|
||||
return
|
||||
}
|
||||
let st
|
||||
try {
|
||||
st = await fsp.stat(absChild)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!st.isFile()) return
|
||||
// mode travels with the file: a wine build's `proton`/`wine` binary is
|
||||
// useless on the other side without its executable bit
|
||||
out.push({ path: relChild, type: 'file', size: st.size, mode: st.mode & 0o777, hash: null })
|
||||
// kept out of the manifest: only this machine can use its own mtimes
|
||||
if (mtimes) mtimes.set(relChild, st.mtimeMs || 0)
|
||||
})
|
||||
queue.push(...dirs)
|
||||
}
|
||||
}
|
||||
|
||||
// Manifest of one sync unit. `force` skips hashing entirely - the receiver
|
||||
// then treats every file as different ("force sync all files").
|
||||
async function buildManifest(roots, force, onProgress) {
|
||||
const entries = []
|
||||
const mtimes = new Map()
|
||||
for (const rel of roots) {
|
||||
const abs = filesPath(rel)
|
||||
if (!abs) continue
|
||||
let st
|
||||
try {
|
||||
st = await fsp.lstat(abs)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (st.isSymbolicLink()) {
|
||||
let target = ''
|
||||
try {
|
||||
target = await fsp.readlink(abs)
|
||||
} catch {}
|
||||
entries.push({ path: rel, type: 'link', target: portableLinkTarget(abs, target) })
|
||||
} else if (st.isDirectory()) {
|
||||
entries.push({ path: rel, type: 'dir' })
|
||||
await scanTree(abs, rel, entries, mtimes)
|
||||
} else if (st.isFile()) {
|
||||
entries.push({ path: rel, type: 'file', size: st.size, mode: st.mode & 0o777, hash: null })
|
||||
mtimes.set(rel, st.mtimeMs || 0)
|
||||
}
|
||||
}
|
||||
const files = entries.filter(e => e.type === 'file')
|
||||
// the unit's total size is known before a byte is hashed, so the bar can
|
||||
// start empty and fill with the bytes actually read
|
||||
const totalBytes = files.reduce((sum, entry) => sum + (entry.size || 0), 0)
|
||||
if (!force && files.length) {
|
||||
let done = 0
|
||||
let hashed = 0
|
||||
await mapLimit(files, concurrency, async entry => {
|
||||
const abs = filesPath(entry.path)
|
||||
try {
|
||||
// identical size and mtime means identical bytes, so the digest from
|
||||
// an earlier sync still describes this file
|
||||
const mtimeMs = mtimes.get(entry.path) || 0
|
||||
entry.hash = abs
|
||||
? await hash.cachedHash(entry.path, abs, entry.size, mtimeMs, bytes => {
|
||||
hashed += bytes
|
||||
if (onProgress) onProgress(done, files.length, hashed, totalBytes)
|
||||
})
|
||||
: null
|
||||
} catch {
|
||||
entry.hash = null
|
||||
}
|
||||
done++
|
||||
if (!entry.hash) hashed += entry.size || 0 // nothing read: still report the file as passed over
|
||||
if (onProgress) onProgress(done, files.length, hashed, totalBytes)
|
||||
})
|
||||
}
|
||||
hash.save()
|
||||
return entries
|
||||
}
|
||||
|
||||
return { buildManifest, scanTree, portableLinkTarget }
|
||||
}
|
||||
|
||||
module.exports = { createManifestBuilder }
|
||||
Loading…
Add table
Add a link
Reference in a new issue