std/fs/copy.ts

404 lines
11 KiB
TypeScript
Raw Normal View History

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { basename } from "@std/path/basename";
import { join } from "@std/path/join";
import { resolve } from "@std/path/resolve";
2019-05-16 16:19:17 +00:00
import { ensureDir, ensureDirSync } from "./ensure_dir.ts";
import { getFileInfoType } from "./_get_file_info_type.ts";
import { toPathString } from "./_to_path_string.ts";
import { isSubdir } from "./_is_subdir.ts";
const isWindows = Deno.build.os === "windows";
/** Options for {@linkcode copy} and {@linkcode copySync}. */
2019-05-16 16:19:17 +00:00
export interface CopyOptions {
/**
* Whether to overwrite existing file or directory.
*
2022-11-25 11:40:23 +00:00
* @default {false}
2019-05-16 16:19:17 +00:00
*/
overwrite?: boolean;
/**
* When `true`, will set last modification and access times to the ones of
* the original source files. When `false`, timestamp behavior is
* OS-dependent.
2022-11-25 11:40:23 +00:00
*
* @default {false}
2019-05-16 16:19:17 +00:00
*/
preserveTimestamps?: boolean;
}
interface InternalCopyOptions extends CopyOptions {
2022-11-25 11:40:23 +00:00
/** @default {false} */
isFolder?: boolean;
}
function assertIsDate(date: Date | null, name: string): asserts date is Date {
if (date === null) {
throw new Error(`${name} is unavailable`);
}
}
2019-05-16 16:19:17 +00:00
async function ensureValidCopy(
2022-08-30 03:58:48 +00:00
src: string | URL,
dest: string | URL,
options: InternalCopyOptions,
): Promise<Deno.FileInfo | undefined> {
let destStat: Deno.FileInfo;
try {
destStat = await Deno.lstat(dest);
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
return;
2019-05-16 16:19:17 +00:00
}
throw err;
2019-05-16 16:19:17 +00:00
}
if (options.isFolder && !destStat.isDirectory) {
throw new Error(
`Cannot overwrite non-directory '${dest}' with directory '${src}'.`,
);
}
if (!options.overwrite) {
throw new Deno.errors.AlreadyExists(`'${dest}' already exists.`);
}
return destStat;
2019-05-16 16:19:17 +00:00
}
function ensureValidCopySync(
2022-08-30 03:58:48 +00:00
src: string | URL,
dest: string | URL,
options: InternalCopyOptions,
): Deno.FileInfo | undefined {
let destStat: Deno.FileInfo;
2019-05-16 16:19:17 +00:00
try {
destStat = Deno.lstatSync(dest);
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
return;
}
throw err;
2019-05-16 16:19:17 +00:00
}
if (options.isFolder && !destStat.isDirectory) {
throw new Error(
`Cannot overwrite non-directory '${dest}' with directory '${src}'.`,
);
}
if (!options.overwrite) {
throw new Deno.errors.AlreadyExists(`'${dest}' already exists.`);
2019-05-16 16:19:17 +00:00
}
return destStat;
2019-05-16 16:19:17 +00:00
}
/* copy file to dest */
async function copyFile(
2022-08-30 03:58:48 +00:00
src: string | URL,
dest: string | URL,
options: InternalCopyOptions,
) {
2019-05-16 16:19:17 +00:00
await ensureValidCopy(src, dest, options);
await Deno.copyFile(src, dest);
if (options.preserveTimestamps) {
const statInfo = await Deno.stat(src);
assertIsDate(statInfo.atime, "statInfo.atime");
assertIsDate(statInfo.mtime, "statInfo.mtime");
await Deno.utime(dest, statInfo.atime, statInfo.mtime);
2019-05-16 16:19:17 +00:00
}
}
/* copy file to dest synchronously */
function copyFileSync(
2022-08-30 03:58:48 +00:00
src: string | URL,
dest: string | URL,
options: InternalCopyOptions,
) {
2019-05-16 16:19:17 +00:00
ensureValidCopySync(src, dest, options);
Deno.copyFileSync(src, dest);
if (options.preserveTimestamps) {
const statInfo = Deno.statSync(src);
assertIsDate(statInfo.atime, "statInfo.atime");
assertIsDate(statInfo.mtime, "statInfo.mtime");
Deno.utimeSync(dest, statInfo.atime, statInfo.mtime);
2019-05-16 16:19:17 +00:00
}
}
/* copy symlink to dest */
async function copySymLink(
2022-08-30 03:58:48 +00:00
src: string | URL,
dest: string | URL,
options: InternalCopyOptions,
) {
2019-05-16 16:19:17 +00:00
await ensureValidCopy(src, dest, options);
const originSrcFilePath = await Deno.readLink(src);
2019-05-16 16:19:17 +00:00
const type = getFileInfoType(await Deno.lstat(src));
if (isWindows) {
await Deno.symlink(originSrcFilePath, dest, {
type: type === "dir" ? "dir" : "file",
});
} else {
await Deno.symlink(originSrcFilePath, dest);
}
2019-05-16 16:19:17 +00:00
if (options.preserveTimestamps) {
const statInfo = await Deno.lstat(src);
assertIsDate(statInfo.atime, "statInfo.atime");
assertIsDate(statInfo.mtime, "statInfo.mtime");
await Deno.utime(dest, statInfo.atime, statInfo.mtime);
2019-05-16 16:19:17 +00:00
}
}
/* copy symlink to dest synchronously */
function copySymlinkSync(
2022-08-30 03:58:48 +00:00
src: string | URL,
dest: string | URL,
options: InternalCopyOptions,
) {
2019-05-16 16:19:17 +00:00
ensureValidCopySync(src, dest, options);
const originSrcFilePath = Deno.readLinkSync(src);
2019-05-16 16:19:17 +00:00
const type = getFileInfoType(Deno.lstatSync(src));
if (isWindows) {
Deno.symlinkSync(originSrcFilePath, dest, {
type: type === "dir" ? "dir" : "file",
});
} else {
Deno.symlinkSync(originSrcFilePath, dest);
}
2019-05-16 16:19:17 +00:00
if (options.preserveTimestamps) {
const statInfo = Deno.lstatSync(src);
assertIsDate(statInfo.atime, "statInfo.atime");
assertIsDate(statInfo.mtime, "statInfo.mtime");
Deno.utimeSync(dest, statInfo.atime, statInfo.mtime);
2019-05-16 16:19:17 +00:00
}
}
/* copy folder from src to dest. */
async function copyDir(
2022-08-30 03:58:48 +00:00
src: string | URL,
dest: string | URL,
options: CopyOptions,
) {
const destStat = await ensureValidCopy(src, dest, {
...options,
isFolder: true,
});
2019-05-16 16:19:17 +00:00
if (!destStat) {
await ensureDir(dest);
}
if (options.preserveTimestamps) {
const srcStatInfo = await Deno.stat(src);
assertIsDate(srcStatInfo.atime, "statInfo.atime");
assertIsDate(srcStatInfo.mtime, "statInfo.mtime");
await Deno.utime(dest, srcStatInfo.atime, srcStatInfo.mtime);
2019-05-16 16:19:17 +00:00
}
2022-08-30 03:58:48 +00:00
src = toPathString(src);
dest = toPathString(dest);
const promises = [];
for await (const entry of Deno.readDir(src)) {
const srcPath = join(src, entry.name);
const destPath = join(dest, basename(srcPath as string));
if (entry.isSymlink) {
promises.push(copySymLink(srcPath, destPath, options));
} else if (entry.isDirectory) {
promises.push(copyDir(srcPath, destPath, options));
} else if (entry.isFile) {
promises.push(copyFile(srcPath, destPath, options));
2019-05-16 16:19:17 +00:00
}
}
await Promise.all(promises);
2019-05-16 16:19:17 +00:00
}
/* copy folder from src to dest synchronously */
2022-08-30 03:58:48 +00:00
function copyDirSync(
src: string | URL,
dest: string | URL,
options: CopyOptions,
) {
const destStat = ensureValidCopySync(src, dest, {
...options,
isFolder: true,
});
2019-05-16 16:19:17 +00:00
if (!destStat) {
ensureDirSync(dest);
}
if (options.preserveTimestamps) {
const srcStatInfo = Deno.statSync(src);
assertIsDate(srcStatInfo.atime, "statInfo.atime");
assertIsDate(srcStatInfo.mtime, "statInfo.mtime");
Deno.utimeSync(dest, srcStatInfo.atime, srcStatInfo.mtime);
2019-05-16 16:19:17 +00:00
}
2022-08-30 03:58:48 +00:00
src = toPathString(src);
dest = toPathString(dest);
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) {
2019-05-16 16:19:17 +00:00
copyDirSync(srcPath, destPath, options);
} else if (entry.isFile) {
2019-05-16 16:19:17 +00:00
copyFileSync(srcPath, destPath, options);
}
}
}
/**
* Asynchronously copy a file or directory. The directory can have contents.
* Like `cp -r`.
*
* If `src` is a directory it will copy everything inside of this directory,
* not the entire directory itself. If `src` is a file, `dest` cannot be a
* directory.
*
* Requires the `--allow-read` and `--allow-write` flag.
2022-11-25 11:40:23 +00:00
*
* @param src The source file/directory path as a string or URL.
* @param dest The destination file/directory path as a string or URL.
* @param options Options for copying.
* @returns A promise that resolves once the copy operation completes.
*
* @example Basic usage
2022-11-25 11:40:23 +00:00
* ```ts
* import { copy } from "@std/fs/copy";
*
* await copy("./foo", "./bar");
* ```
*
* This will copy the file or directory at `./foo` to `./bar` without
* overwriting.
*
* @example Overwriting files/directories
* ```ts
* import { copy } from "@std/fs/copy";
*
* await copy("./foo", "./bar", { overwrite: true });
* ```
*
* This will copy the file or directory at `./foo` to `./bar` and overwrite
* any existing files or directories.
*
* @example Preserving timestamps
* ```ts
* import { copy } from "@std/fs/copy";
*
* await copy("./foo", "./bar", { preserveTimestamps: true });
2022-11-25 11:40:23 +00:00
* ```
*
* This will copy the file or directory at `./foo` to `./bar` and set the
* last modification and access times to the ones of the original source files.
2019-05-16 16:19:17 +00:00
*/
export async function copy(
2022-08-30 03:58:48 +00:00
src: string | URL,
dest: string | URL,
options: CopyOptions = {},
) {
src = resolve(toPathString(src));
dest = resolve(toPathString(dest));
2019-05-16 16:19:17 +00:00
if (src === dest) {
throw new Error("Source and destination cannot be the same.");
}
const srcStat = await Deno.lstat(src);
if (srcStat.isDirectory && isSubdir(src, dest)) {
2019-05-16 16:19:17 +00:00
throw new Error(
`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`,
2019-05-16 16:19:17 +00:00
);
}
if (srcStat.isSymlink) {
await copySymLink(src, dest, options);
} else if (srcStat.isDirectory) {
2019-05-16 16:19:17 +00:00
await copyDir(src, dest, options);
} else if (srcStat.isFile) {
2019-05-16 16:19:17 +00:00
await copyFile(src, dest, options);
}
}
/**
* Synchronously copy a file or directory. The directory can have contents.
* Like `cp -r`.
*
* If `src` is a directory it will copy everything inside of this directory,
* not the entire directory itself. If `src` is a file, `dest` cannot be a
* directory.
*
* Requires the `--allow-read` and `--allow-write` flag.
2022-11-25 11:40:23 +00:00
*
* @param src The source file/directory path as a string or URL.
* @param dest The destination file/directory path as a string or URL.
* @param options Options for copying.
* @returns A void value that returns once the copy operation completes.
*
* @example Basic usage
2022-11-25 11:40:23 +00:00
* ```ts
* import { copySync } from "@std/fs/copy";
*
* copySync("./foo", "./bar");
2022-11-25 11:40:23 +00:00
* ```
*
* This will copy the file or directory at `./foo` to `./bar` without
* overwriting.
*
* @example Overwriting files/directories
* ```ts
* import { copySync } from "@std/fs/copy";
*
* copySync("./foo", "./bar", { overwrite: true });
* ```
*
* This will copy the file or directory at `./foo` to `./bar` and overwrite
* any existing files or directories.
*
* @example Preserving timestamps
* ```ts
* import { copySync } from "@std/fs/copy";
*
* copySync("./foo", "./bar", { preserveTimestamps: true });
* ```
*
* This will copy the file or directory at `./foo` to `./bar` and set the
* last modification and access times to the ones of the original source files.
2019-05-16 16:19:17 +00:00
*/
export function copySync(
2022-08-30 03:58:48 +00:00
src: string | URL,
dest: string | URL,
options: CopyOptions = {},
) {
src = resolve(toPathString(src));
dest = resolve(toPathString(dest));
2019-05-16 16:19:17 +00:00
if (src === dest) {
throw new Error("Source and destination cannot be the same.");
}
const srcStat = Deno.lstatSync(src);
if (srcStat.isDirectory && isSubdir(src, dest)) {
2019-05-16 16:19:17 +00:00
throw new Error(
`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`,
2019-05-16 16:19:17 +00:00
);
}
if (srcStat.isSymlink) {
copySymlinkSync(src, dest, options);
} else if (srcStat.isDirectory) {
2019-05-16 16:19:17 +00:00
copyDirSync(src, dest, options);
} else if (srcStat.isFile) {
2019-05-16 16:19:17 +00:00
copyFileSync(src, dest, options);
}
}