sync-content
Version:
Synchronize the contents of one folder to another location, only copying files if contents differ.
285 lines • 9.16 kB
JavaScript
import { glob, globSync } from 'glob';
import { mkdirp, mkdirpSync } from 'mkdirp';
import constants from 'node:constants';
import { createHash } from 'node:crypto';
import { chmodSync, copyFileSync, linkSync, readFileSync, readlinkSync, symlinkSync, } from 'node:fs';
import { chmod, copyFile, link, readFile, readlink, symlink, } from 'node:fs/promises';
import { dirname, relative, resolve, sep } from 'node:path';
import { PathScurry } from 'path-scurry';
import { rimraf, rimrafSync } from 'rimraf';
const mkdirpClobber = async (dir) => {
try {
return await mkdirp(dir);
}
catch (e) {
await mkdirpClobber(dirname(dir));
await rimraf(dir);
return await mkdirp(dir);
}
};
const mkdirpClobberSync = (dir) => {
try {
return mkdirpSync(dir);
}
catch (e) {
mkdirpClobberSync(dirname(dir));
rimrafSync(dir);
return mkdirpSync(dir);
}
};
const syncFile = async (src, dest) => {
// only sync files, dirs, and symlinks
// Creating these is a pain to test cross platform
/* c8 ignore start */
if (!(src.isSymbolicLink() || src.isDirectory() || src.isFile())) {
if (!dest.isENOENT())
await rimraf(dest.fullpath());
return;
}
/* c8 ignore stop */
if (src.isSymbolicLink()) {
const target = await readlink(src.fullpath());
if (!dest.isENOENT()) {
const dp = dest.isSymbolicLink() && (await readlink(dest.fullpath()));
if (dp === target)
return;
await rimraf(dest.fullpath());
}
await mkdirpClobber(dirname(dest.fullpath()));
await symlink(target, dest.fullpath());
/* c8 ignore start */
if (constants.O_SYMLINK && src.mode) {
try {
await chmod(dest.fullpath(), src.mode);
}
catch { }
}
/* c8 ignore stop */
return;
}
if (src.isDirectory()) {
if (!dest.isDirectory()) {
await mkdirpClobber(dest.fullpath());
}
}
else {
// must be file
let write = true;
if (!dest.isENOENT() && !dest.isFile())
await rimraf(dest.fullpath());
else if (await contentMatch(src, dest))
write = false;
if (write) {
await mkdirpClobber(dirname(dest.fullpath()));
// platform specific
/* c8 ignore start */
await link(src.fullpath(), dest.fullpath()).catch(() => copyFile(src.fullpath(), dest.fullpath()));
/* c8 ignore stop */
}
}
const mode = src.mode;
/* c8 ignore start */
if (!mode)
return;
/* c8 ignore stop */
await chmod(dest.fullpath(), mode);
};
const syncFileSync = (src, dest) => {
// only sync files, dirs, and symlinks
// Creating these is a pain to test cross platform
/* c8 ignore start */
if (!(src.isSymbolicLink() || src.isDirectory() || src.isFile())) {
if (!dest.isENOENT())
rimrafSync(dest.fullpath());
return;
}
/* c8 ignore stop */
if (src.isSymbolicLink()) {
const target = readlinkSync(src.fullpath());
if (!dest.isENOENT()) {
const dp = dest.isSymbolicLink() && readlinkSync(dest.fullpath());
if (dp === target)
return;
rimrafSync(dest.fullpath());
}
mkdirpClobberSync(dirname(dest.fullpath()));
symlinkSync(target, dest.fullpath());
/* c8 ignore start */
if (constants.O_SYMLINK && src.mode) {
try {
chmodSync(dest.fullpath(), src.mode);
}
catch { }
}
/* c8 ignore stop */
return;
}
if (src.isDirectory()) {
if (!dest.isDirectory()) {
mkdirpClobberSync(dest.fullpath());
}
}
else {
// must be file
let write = true;
if (!dest.isENOENT() && !dest.isFile())
rimrafSync(dest.fullpath());
else if (contentMatchSync(src, dest))
write = false;
if (write) {
mkdirpClobberSync(dirname(dest.fullpath()));
// platform specific
/* c8 ignore start */
try {
linkSync(src.fullpath(), dest.fullpath());
}
catch {
copyFileSync(src.fullpath(), dest.fullpath());
}
/* c8 ignore stop */
}
}
const mode = src.mode;
/* c8 ignore start */
if (!mode)
return;
/* c8 ignore stop */
chmodSync(dest.fullpath(), mode);
};
const contentMatch = async (src, dest) => {
try {
return (createHash('sha512')
.update(await readFile(src.fullpath()))
.digest('hex') ===
createHash('sha512')
.update(await readFile(dest.fullpath()))
.digest('hex'));
// we should only be doing this if we know it's a valid file already
// but just in case we can't read it, that's not a match.
/* c8 ignore start */
}
catch {
return false;
}
/* c8 ignore stop */
};
const contentMatchSync = (src, dest) => {
try {
return (createHash('sha512')
.update(readFileSync(src.fullpath()))
.digest('hex') ===
createHash('sha512')
.update(readFileSync(dest.fullpath()))
.digest('hex'));
// we should only be doing this if we know it's a valid file already
// but just in case we can't read it, that's not a match.
/* c8 ignore start */
}
catch {
return false;
}
/* c8 ignore stop */
};
// if a is a parent of b, or b is a parent of a, then one of them
// will not start with .. in the relative path.
const dots = `..${sep}`;
const dirsRelated = (a, b) => {
if (a === b)
return true;
const relab = relative(a, b);
const relba = relative(a, b);
if (!relab.startsWith(dots) || !relba.startsWith(dots))
return true;
return false;
};
export const syncContent = async (from, to) => {
const scurry = new PathScurry(from);
const rfrom = resolve(from);
const rto = resolve(to);
if (dirname(rfrom) === rfrom || dirname(rto) === rto) {
throw new Error('cannot sync root directory');
}
/* c8 ignore start */
if (dirsRelated(rto, rfrom)) {
/* c8 ignore stop */
throw new Error('cannot copy directory into itself or its parent');
}
const [src, dest] = await Promise.all([
await glob('**', { scurry, withFileTypes: true }),
await glob('**', { cwd: rto, withFileTypes: true }),
]);
await Promise.all([
...src.map(async (s) => {
/* c8 ignore start */
if (!s.parent)
throw new Error('cannot sync root directory');
/* c8 ignore stop */
const d = s.resolve(resolve(rto, s.relative()));
const parent = d.parent;
/* c8 ignore start */
if (!parent)
throw new Error('cannot sync root directory');
/* c8 ignore stop */
await d.lstat();
await mkdirpClobber(parent.fullpath());
await syncFile(s, d);
}),
...dest.map(async (d) => {
const s = scurry.cwd.resolve(resolve(rfrom, d.relative()));
await s.lstat();
if (s.isENOENT()) {
// race
/* c8 ignore start */
try {
await rimraf(d.fullpath());
}
catch { }
/* c8 ignore stop */
}
}),
]);
};
export const syncContentSync = (from, to) => {
const scurry = new PathScurry(from);
const rfrom = resolve(from);
const rto = resolve(to);
if (dirname(rfrom) === rfrom || dirname(rto) === rto) {
throw new Error('cannot sync root directory');
}
if (dirsRelated(rto, rfrom)) {
throw new Error('cannot copy directory into itself or its parent');
}
const [src, dest] = [
globSync('**', { scurry, withFileTypes: true }),
globSync('**', { cwd: rto, withFileTypes: true }),
];
for (const s of src) {
/* c8 ignore start */
if (!s.parent)
throw new Error('cannot sync root directory');
/* c8 ignore stop */
const d = s.resolve(resolve(rto, s.relative()));
const parent = d.parent;
/* c8 ignore start */
if (!parent)
throw new Error('cannot sync root directory');
/* c8 ignore stop */
d.lstatSync();
mkdirpClobberSync(parent.fullpath());
syncFileSync(s, d);
}
for (const d of dest) {
const s = scurry.cwd.resolve(resolve(rfrom, d.relative()));
s.lstatSync();
if (s.isENOENT()) {
// race
/* c8 ignore start */
try {
rimrafSync(d.fullpath());
}
catch { }
/* c8 ignore stop */
}
}
};
//# sourceMappingURL=index.js.map