import {
basename,
dirname,
join,
relative,
resolve,
} from "../deno_ral/path.ts";
import {
CopyOptions,
ensureDirSync,
existsSync,
getFileInfoType,
isSubdir,
safeRemoveSync,
walkSync,
} from "../deno_ral/fs.ts";
import { isWindows } from "../deno_ral/platform.ts";
export function copyTo(
src: string,
dest: string,
options: CopyOptions = {
overwrite: true,
},
) {
src = resolve(src);
dest = resolve(dest);
if (src === dest) {
throw new Error("Source and destination cannot be the same.");
}
const srcStat = Deno.lstatSync(src);
if (srcStat.isDirectory && isSubdir(src, dest)) {
throw new Error(
`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`,
);
}
if (srcStat.isSymlink) {
copySymlinkSync(src, dest, options);
} else if (srcStat.isDirectory) {
copyDirSync(src, dest, options);
} else if (srcStat.isFile) {
copyFileSync(src, dest, options);
}
}
export function copyMinimal(
srcDir: string,
destDir: string,
skip?: RegExp[],
filter?: (path: string) => boolean,
) {
skip = [...(skip || []), /\.DS_Store/];
const srcFiles: string[] = [];
for (
const walk of walkSync(
srcDir,
{
includeDirs: false,
followSymlinks: false,
skip,
},
)
) {
const srcFile = walk.path;
if (filter && !filter(srcFile)) {
continue;
}
srcFiles.push(srcFile);
}
for (const srcFile of srcFiles) {
if (!existsSync(srcFile)) {
continue;
}
const destFile = join(destDir, relative(srcDir, srcFile));
copyFileIfNewer(srcFile, destFile);
}
}
export function copyFileIfNewer(srcFile: string, destFile: string) {
const doCopy = () => {
copyTo(srcFile, destFile, {
overwrite: true,
preserveTimestamps: true,
});
};
ensureDirSync(dirname(destFile));
try {
if (existsSync(destFile)) {
const srcInfo = Deno.statSync(srcFile);
const destInfo = Deno.statSync(destFile);
if (!srcInfo.mtime || !destInfo.mtime || destInfo.mtime < srcInfo.mtime) {
doCopy();
}
} else {
doCopy();
}
} catch {
doCopy();
}
}
function copyFileSync(
src: string,
dest: string,
options: InternalCopyOptions,
): void {
ensureValidCopySync(src, dest, options);
if (existsSync(dest)) {
safeRemoveSync(dest);
}
Deno.copyFileSync(src, dest);
if (options.preserveTimestamps) {
const statInfo = Deno.statSync(src);
if (statInfo.atime && statInfo.mtime) {
Deno.utimeSync(dest, statInfo.atime, statInfo.mtime);
}
}
}
function copyDirSync(src: string, dest: string, options: CopyOptions): void {
const destStat = ensureValidCopySync(src, dest, {
...options,
isFolder: true,
});
if (!destStat) {
ensureDirSync(dest);
}
if (options.preserveTimestamps) {
const srcStatInfo = Deno.statSync(src);
if (srcStatInfo.atime && srcStatInfo.mtime) {
Deno.utimeSync(dest, srcStatInfo.atime, srcStatInfo.mtime);
}
}
for (const entry of Deno.readDirSync(src)) {
const srcPath = join(src, entry.name);
const destPath = join(dest, basename(srcPath as string));
if (entry.isSymlink) {
copySymlinkSync(srcPath, destPath, options);
} else if (entry.isDirectory) {
copyDirSync(srcPath, destPath, options);
} else if (entry.isFile) {
copyFileSync(srcPath, destPath, options);
}
}
}
function copySymlinkSync(
src: string,
dest: string,
options: InternalCopyOptions,
): void {
ensureValidCopySync(src, dest, options);
if (existsSync(dest)) {
safeRemoveSync(dest);
}
const originSrcFilePath = Deno.readLinkSync(src);
const type = getFileInfoType(Deno.lstatSync(src));
if (isWindows) {
Deno.symlinkSync(originSrcFilePath, dest, {
type: type === "dir" ? "dir" : "file",
});
} else {
Deno.symlinkSync(originSrcFilePath, dest);
}
if (options.preserveTimestamps) {
const statInfo = Deno.lstatSync(src);
if (statInfo.atime && statInfo.mtime) {
Deno.utimeSync(dest, statInfo.atime, statInfo.mtime);
}
}
}
interface InternalCopyOptions extends CopyOptions {
isFolder?: boolean;
}
function ensureValidCopySync(
src: string,
dest: string,
options: InternalCopyOptions,
): Deno.FileInfo | undefined {
let destStat: Deno.FileInfo;
try {
destStat = Deno.lstatSync(dest);
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
return;
}
throw err;
}
if (options.isFolder && !destStat.isDirectory) {
throw new Error(
`Cannot overwrite non-directory '${dest}' with directory '${src}'.`,
);
}
if (!options.overwrite) {
throw new Error(`'${dest}' already exists.`);
}
return destStat;
}