summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAxetroy <axetroy.dev@gmail.com>2019-05-17 00:19:17 +0800
committerRyan Dahl <ry@tinyclouds.org>2019-05-16 12:19:17 -0400
commitf7dd691cfb953f7bc1419fe26af5e18f38ab7e84 (patch)
treebed935e73d2d0b752cb524243d33c16e7be556ff
parent42a00733fcf4d04f35e7676db560495b8999ddfa (diff)
feat(fs): add copy/copySync (denoland/deno_std#278)
Original: https://github.com/denoland/deno_std/commit/ab5b0887ccde0ad9f00f1943e27bd73bc483833b
-rw-r--r--fs/README.md13
-rw-r--r--fs/copy.ts260
-rw-r--r--fs/copy_test.ts558
-rw-r--r--fs/mod.ts1
-rw-r--r--fs/test.ts1
-rw-r--r--fs/testdata/copy_dir/0.txt1
-rw-r--r--fs/testdata/copy_dir/nest/0.txt1
l---------fs/testdata/copy_dir_link_file/0.txt1
-rw-r--r--fs/testdata/copy_file.txt1
9 files changed, 837 insertions, 0 deletions
diff --git a/fs/README.md b/fs/README.md
index 01780892d..8f4c472a8 100644
--- a/fs/README.md
+++ b/fs/README.md
@@ -129,6 +129,19 @@ moveSync("./foo", "./existingFolder", { overwrite: true });
// Will overwrite existingFolder
```
+### copy
+
+copy a file or directory. Overwrites it if option provided
+
+```ts
+import { copy, copySync } from "https://deno.land/std/fs/mod.ts";
+
+copy("./foo", "./bar"); // returns a promise
+copySync("./foo", "./bar"); // void
+copySync("./foo", "./existingFolder", { overwrite: true });
+// Will overwrite existingFolder
+```
+
### readJson
Reads a JSON file and then parses it into an object
diff --git a/fs/copy.ts b/fs/copy.ts
new file mode 100644
index 000000000..83c4e73ad
--- /dev/null
+++ b/fs/copy.ts
@@ -0,0 +1,260 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as path from "./path/mod.ts";
+import { ensureDir, ensureDirSync } from "./ensure_dir.ts";
+import { isSubdir, getFileInfoType } from "./utils.ts";
+
+export interface CopyOptions {
+ /**
+ * overwrite existing file or directory. Default is `false`
+ */
+ 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.
+ * Default is `false`.
+ */
+ preserveTimestamps?: boolean;
+}
+
+async function ensureValidCopy(
+ src: string,
+ dest: string,
+ options: CopyOptions,
+ isCopyFolder: boolean = false
+): Promise<Deno.FileInfo> {
+ let destStat: Deno.FileInfo;
+
+ destStat = await Deno.lstat(dest).catch(
+ (): Promise<null> => Promise.resolve(null)
+ );
+
+ if (destStat) {
+ if (isCopyFolder && !destStat.isDirectory()) {
+ throw new Error(
+ `Cannot overwrite non-directory '${dest}' with directory '${src}'.`
+ );
+ }
+ if (!options.overwrite) {
+ throw new Error(`'${dest}' already exists.`);
+ }
+ }
+
+ return destStat;
+}
+
+function ensureValidCopySync(
+ src: string,
+ dest: string,
+ options: CopyOptions,
+ isCopyFolder: boolean = false
+): Deno.FileInfo {
+ let destStat: Deno.FileInfo;
+
+ try {
+ destStat = Deno.lstatSync(dest);
+ } catch {
+ // ignore error
+ }
+
+ if (destStat) {
+ if (isCopyFolder && !destStat.isDirectory()) {
+ throw new Error(
+ `Cannot overwrite non-directory '${dest}' with directory '${src}'.`
+ );
+ }
+ if (!options.overwrite) {
+ throw new Error(`'${dest}' already exists.`);
+ }
+ }
+
+ return destStat;
+}
+
+/* copy file to dest */
+async function copyFile(
+ src: string,
+ dest: string,
+ options: CopyOptions
+): Promise<void> {
+ await ensureValidCopy(src, dest, options);
+ await Deno.copyFile(src, dest);
+ if (options.preserveTimestamps) {
+ const statInfo = await Deno.stat(src);
+ await Deno.utime(dest, statInfo.accessed, statInfo.modified);
+ }
+}
+/* copy file to dest synchronously */
+function copyFileSync(src: string, dest: string, options: CopyOptions): void {
+ ensureValidCopySync(src, dest, options);
+ Deno.copyFileSync(src, dest);
+ if (options.preserveTimestamps) {
+ const statInfo = Deno.statSync(src);
+ Deno.utimeSync(dest, statInfo.accessed, statInfo.modified);
+ }
+}
+
+/* copy symlink to dest */
+async function copySymLink(
+ src: string,
+ dest: string,
+ options: CopyOptions
+): Promise<void> {
+ await ensureValidCopy(src, dest, options);
+ const originSrcFilePath = await Deno.readlink(src);
+ const type = getFileInfoType(await Deno.lstat(src));
+ await Deno.symlink(originSrcFilePath, dest, type);
+ if (options.preserveTimestamps) {
+ const statInfo = await Deno.lstat(src);
+ await Deno.utime(dest, statInfo.accessed, statInfo.modified);
+ }
+}
+
+/* copy symlink to dest synchronously */
+function copySymlinkSync(
+ src: string,
+ dest: string,
+ options: CopyOptions
+): void {
+ ensureValidCopySync(src, dest, options);
+ const originSrcFilePath = Deno.readlinkSync(src);
+ const type = getFileInfoType(Deno.lstatSync(src));
+ Deno.symlinkSync(originSrcFilePath, dest, type);
+ if (options.preserveTimestamps) {
+ const statInfo = Deno.lstatSync(src);
+ Deno.utimeSync(dest, statInfo.accessed, statInfo.modified);
+ }
+}
+
+/* copy folder from src to dest. */
+async function copyDir(
+ src: string,
+ dest: string,
+ options: CopyOptions
+): Promise<void> {
+ const destStat = await ensureValidCopy(src, dest, options, true);
+
+ if (!destStat) {
+ await ensureDir(dest);
+ }
+
+ if (options.preserveTimestamps) {
+ const srcStatInfo = await Deno.stat(src);
+ await Deno.utime(dest, srcStatInfo.accessed, srcStatInfo.modified);
+ }
+
+ const files = await Deno.readDir(src);
+
+ for (const file of files) {
+ const srcPath = file.path as string;
+ const destPath = path.join(dest, path.basename(srcPath as string));
+ if (file.isDirectory()) {
+ await copyDir(srcPath, destPath, options);
+ } else if (file.isFile()) {
+ await copyFile(srcPath, destPath, options);
+ } else if (file.isSymlink()) {
+ await copySymLink(srcPath, destPath, options);
+ }
+ }
+}
+
+/* copy folder from src to dest synchronously */
+function copyDirSync(src: string, dest: string, options: CopyOptions): void {
+ const destStat: Deno.FileInfo = ensureValidCopySync(src, dest, options, true);
+
+ if (!destStat) {
+ ensureDirSync(dest);
+ }
+
+ if (options.preserveTimestamps) {
+ const srcStatInfo = Deno.statSync(src);
+ Deno.utimeSync(dest, srcStatInfo.accessed, srcStatInfo.modified);
+ }
+
+ const files = Deno.readDirSync(src);
+
+ for (const file of files) {
+ const srcPath = file.path as string;
+ const destPath = path.join(dest, path.basename(srcPath as string));
+ if (file.isDirectory()) {
+ copyDirSync(srcPath, destPath, options);
+ } else if (file.isFile()) {
+ copyFileSync(srcPath, destPath, options);
+ } else if (file.isSymlink()) {
+ copySymlinkSync(srcPath, destPath, options);
+ }
+ }
+}
+
+/**
+ * Copy a file or directory. The directory can have contents. Like `cp -r`.
+ * @param src the file/directory path.
+ * Note that if `src` is a directory it will copy everything inside of this directory,
+ * not the entire directory itself
+ * @param dest the destination path. Note that if `src` is a file, `dest` cannot be a directory
+ * @param options
+ */
+export async function copy(
+ src: string,
+ dest: string,
+ options: CopyOptions = {}
+): Promise<void> {
+ src = path.resolve(src);
+ dest = path.resolve(dest);
+
+ 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)) {
+ throw new Error(
+ `Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`
+ );
+ }
+
+ if (srcStat.isDirectory()) {
+ await copyDir(src, dest, options);
+ } else if (srcStat.isFile()) {
+ await copyFile(src, dest, options);
+ } else if (srcStat.isSymlink()) {
+ await copySymLink(src, dest, options);
+ }
+}
+
+/**
+ * Copy a file or directory. The directory can have contents. Like `cp -r`.
+ * @param src the file/directory path.
+ * Note that if `src` is a directory it will copy everything inside of this directory,
+ * not the entire directory itself
+ * @param dest the destination path. Note that if `src` is a file, `dest` cannot be a directory
+ * @param options
+ */
+export function copySync(
+ src: string,
+ dest: string,
+ options: CopyOptions = {}
+): void {
+ src = path.resolve(src);
+ dest = path.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.isDirectory()) {
+ copyDirSync(src, dest, options);
+ } else if (srcStat.isFile()) {
+ copyFileSync(src, dest, options);
+ } else if (srcStat.isSymlink()) {
+ copySymlinkSync(src, dest, options);
+ }
+}
diff --git a/fs/copy_test.ts b/fs/copy_test.ts
new file mode 100644
index 000000000..03d439ec1
--- /dev/null
+++ b/fs/copy_test.ts
@@ -0,0 +1,558 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test } from "../testing/mod.ts";
+import {
+ assertEquals,
+ assertThrows,
+ assertThrowsAsync,
+ assert
+} from "../testing/asserts.ts";
+import { copy, copySync } from "./copy.ts";
+import { exists, existsSync } from "./exists.ts";
+import * as path from "./path/mod.ts";
+import { ensureDir, ensureDirSync } from "./ensure_dir.ts";
+import { ensureFile, ensureFileSync } from "./ensure_file.ts";
+import { ensureSymlink, ensureSymlinkSync } from "./ensure_symlink.ts";
+
+const testdataDir = path.resolve("fs", "testdata");
+
+// TODO(axetroy): Add test for Windows once symlink is implemented for Windows.
+const isWindows = Deno.platform.os === "win";
+
+async function testCopy(
+ name: string,
+ cb: (tempDir: string) => Promise<void>
+): Promise<void> {
+ test({
+ name,
+ async fn(): Promise<void> {
+ const tempDir = await Deno.makeTempDir({
+ prefix: "deno_std_copy_async_test_"
+ });
+ await cb(tempDir);
+ await Deno.remove(tempDir, { recursive: true });
+ }
+ });
+}
+
+function testCopySync(name: string, cb: (tempDir: string) => void): void {
+ test({
+ name,
+ fn: (): void => {
+ const tempDir = Deno.makeTempDirSync({
+ prefix: "deno_std_copy_sync_test_"
+ });
+ cb(tempDir);
+ Deno.removeSync(tempDir, { recursive: true });
+ }
+ });
+}
+
+testCopy(
+ "[fs] copy file if it does no exist",
+ async (tempDir: string): Promise<void> => {
+ const srcFile = path.join(testdataDir, "copy_file_not_exists.txt");
+ const destFile = path.join(tempDir, "copy_file_not_exists_1.txt");
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ await copy(srcFile, destFile);
+ }
+ );
+ }
+);
+
+testCopy(
+ "[fs] copy if src and dest are the same paths",
+ async (tempDir: string): Promise<void> => {
+ const srcFile = path.join(tempDir, "copy_file_same.txt");
+ const destFile = path.join(tempDir, "copy_file_same.txt");
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ await copy(srcFile, destFile);
+ },
+ Error,
+ "Source and destination cannot be the same."
+ );
+ }
+);
+
+testCopy(
+ "[fs] copy file",
+ async (tempDir: string): Promise<void> => {
+ const srcFile = path.join(testdataDir, "copy_file.txt");
+ const destFile = path.join(tempDir, "copy_file_copy.txt");
+
+ const srcContent = new TextDecoder().decode(await Deno.readFile(srcFile));
+
+ assertEquals(
+ await exists(srcFile),
+ true,
+ `source should exist before copy`
+ );
+ assertEquals(
+ await exists(destFile),
+ false,
+ "destination should not exist before copy"
+ );
+
+ await copy(srcFile, destFile);
+
+ assertEquals(await exists(srcFile), true, "source should exist after copy");
+ assertEquals(
+ await exists(destFile),
+ true,
+ "destination should exist before copy"
+ );
+
+ const destContent = new TextDecoder().decode(await Deno.readFile(destFile));
+
+ assertEquals(
+ srcContent,
+ destContent,
+ "source and destination should have the same content"
+ );
+
+ // Copy again and it should throw an error.
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ await copy(srcFile, destFile);
+ },
+ Error,
+ `'${destFile}' already exists.`
+ );
+
+ // Modify destination file.
+ await Deno.writeFile(destFile, new TextEncoder().encode("txt copy"));
+
+ assertEquals(
+ new TextDecoder().decode(await Deno.readFile(destFile)),
+ "txt copy"
+ );
+
+ // Copy again with overwrite option.
+ await copy(srcFile, destFile, { overwrite: true });
+
+ // Make sure the file has been overwritten.
+ assertEquals(
+ new TextDecoder().decode(await Deno.readFile(destFile)),
+ "txt"
+ );
+ }
+);
+
+testCopy(
+ "[fs] copy with preserve timestamps",
+ async (tempDir: string): Promise<void> => {
+ const srcFile = path.join(testdataDir, "copy_file.txt");
+ const destFile = path.join(tempDir, "copy_file_copy.txt");
+
+ const srcStatInfo = await Deno.stat(srcFile);
+
+ assert(typeof srcStatInfo.accessed === "number");
+ assert(typeof srcStatInfo.modified === "number");
+
+ // Copy with overwrite and preserve timestamps options.
+ await copy(srcFile, destFile, {
+ overwrite: true,
+ preserveTimestamps: true
+ });
+
+ const destStatInfo = await Deno.stat(destFile);
+
+ assert(typeof destStatInfo.accessed === "number");
+ assert(typeof destStatInfo.modified === "number");
+ assertEquals(destStatInfo.accessed, srcStatInfo.accessed);
+ assertEquals(destStatInfo.modified, srcStatInfo.modified);
+ }
+);
+
+testCopy(
+ "[fs] copy directory to its subdirectory",
+ async (tempDir: string): Promise<void> => {
+ const srcDir = path.join(tempDir, "parent");
+ const destDir = path.join(srcDir, "child");
+
+ await ensureDir(srcDir);
+
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ await copy(srcDir, destDir);
+ },
+ Error,
+ `Cannot copy '${srcDir}' to a subdirectory of itself, '${destDir}'.`
+ );
+ }
+);
+
+testCopy(
+ "[fs] copy directory and destination exist and not a directory",
+ async (tempDir: string): Promise<void> => {
+ const srcDir = path.join(tempDir, "parent");
+ const destDir = path.join(tempDir, "child.txt");
+
+ await ensureDir(srcDir);
+ await ensureFile(destDir);
+
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ await copy(srcDir, destDir);
+ },
+ Error,
+ `Cannot overwrite non-directory '${destDir}' with directory '${srcDir}'.`
+ );
+ }
+);
+
+testCopy(
+ "[fs] copy directory",
+ async (tempDir: string): Promise<void> => {
+ const srcDir = path.join(testdataDir, "copy_dir");
+ const destDir = path.join(tempDir, "copy_dir");
+ const srcFile = path.join(srcDir, "0.txt");
+ const destFile = path.join(destDir, "0.txt");
+ const srcNestFile = path.join(srcDir, "nest", "0.txt");
+ const destNestFile = path.join(destDir, "nest", "0.txt");
+
+ await copy(srcDir, destDir);
+
+ assertEquals(await exists(destFile), true);
+ assertEquals(await exists(destNestFile), true);
+
+ // After copy. The source and destination should have the same content.
+ assertEquals(
+ new TextDecoder().decode(await Deno.readFile(srcFile)),
+ new TextDecoder().decode(await Deno.readFile(destFile))
+ );
+ assertEquals(
+ new TextDecoder().decode(await Deno.readFile(srcNestFile)),
+ new TextDecoder().decode(await Deno.readFile(destNestFile))
+ );
+
+ // Copy again without overwrite option and it should throw an error.
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ await copy(srcDir, destDir);
+ },
+ Error,
+ `'${destDir}' already exists.`
+ );
+
+ // Modify the file in the destination directory.
+ await Deno.writeFile(destNestFile, new TextEncoder().encode("nest copy"));
+ assertEquals(
+ new TextDecoder().decode(await Deno.readFile(destNestFile)),
+ "nest copy"
+ );
+
+ // Copy again with overwrite option.
+ await copy(srcDir, destDir, { overwrite: true });
+
+ // Make sure the file has been overwritten.
+ assertEquals(
+ new TextDecoder().decode(await Deno.readFile(destNestFile)),
+ "nest"
+ );
+ }
+);
+
+testCopy(
+ "[fs] copy symlink file",
+ async (tempDir: string): Promise<void> => {
+ const dir = path.join(testdataDir, "copy_dir_link_file");
+ const srcLink = path.join(dir, "0.txt");
+ const destLink = path.join(tempDir, "0_copy.txt");
+
+ if (isWindows) {
+ await assertThrowsAsync(
+ // (): Promise<void> => copy(srcLink, destLink),
+ (): Promise<void> => ensureSymlink(srcLink, destLink)
+ );
+ return;
+ }
+
+ assert(
+ (await Deno.lstat(srcLink)).isSymlink(),
+ `'${srcLink}' should be symlink type`
+ );
+
+ await copy(srcLink, destLink);
+
+ const statInfo = await Deno.lstat(destLink);
+
+ assert(statInfo.isSymlink(), `'${destLink}' should be symlink type`);
+ }
+);
+
+testCopy(
+ "[fs] copy symlink directory",
+ async (tempDir: string): Promise<void> => {
+ const srcDir = path.join(testdataDir, "copy_dir");
+ const srcLink = path.join(tempDir, "copy_dir_link");
+ const destLink = path.join(tempDir, "copy_dir_link_copy");
+
+ if (isWindows) {
+ await assertThrowsAsync(
+ // (): Promise<void> => copy(srcLink, destLink),
+ (): Promise<void> => ensureSymlink(srcLink, destLink)
+ );
+ return;
+ }
+
+ await ensureSymlink(srcDir, srcLink);
+
+ assert(
+ (await Deno.lstat(srcLink)).isSymlink(),
+ `'${srcLink}' should be symlink type`
+ );
+
+ await copy(srcLink, destLink);
+
+ const statInfo = await Deno.lstat(destLink);
+
+ assert(statInfo.isSymlink());
+ }
+);
+
+testCopySync(
+ "[fs] copy file synchronously if it does not exist",
+ (tempDir: string): void => {
+ const srcFile = path.join(testdataDir, "copy_file_not_exists_sync.txt");
+ const destFile = path.join(tempDir, "copy_file_not_exists_1_sync.txt");
+ assertThrows(
+ (): void => {
+ copySync(srcFile, destFile);
+ }
+ );
+ }
+);
+
+testCopySync(
+ "[fs] copy synchronously with preserve timestamps",
+ (tempDir: string): void => {
+ const srcFile = path.join(testdataDir, "copy_file.txt");
+ const destFile = path.join(tempDir, "copy_file_copy.txt");
+
+ const srcStatInfo = Deno.statSync(srcFile);
+
+ assert(typeof srcStatInfo.accessed === "number");
+ assert(typeof srcStatInfo.modified === "number");
+
+ // Copy with overwrite and preserve timestamps options.
+ copySync(srcFile, destFile, {
+ overwrite: true,
+ preserveTimestamps: true
+ });
+
+ const destStatInfo = Deno.statSync(destFile);
+
+ assert(typeof destStatInfo.accessed === "number");
+ assert(typeof destStatInfo.modified === "number");
+ assertEquals(destStatInfo.accessed, srcStatInfo.accessed);
+ assertEquals(destStatInfo.modified, srcStatInfo.modified);
+ }
+);
+
+testCopySync(
+ "[fs] copy synchronously if src and dest are the same paths",
+ (): void => {
+ const srcFile = path.join(testdataDir, "copy_file_same_sync.txt");
+ assertThrows(
+ (): void => {
+ copySync(srcFile, srcFile);
+ },
+ Error,
+ "Source and destination cannot be the same."
+ );
+ }
+);
+
+testCopySync(
+ "[fs] copy file synchronously",
+ (tempDir: string): void => {
+ const srcFile = path.join(testdataDir, "copy_file.txt");
+ const destFile = path.join(tempDir, "copy_file_copy_sync.txt");
+
+ const srcContent = new TextDecoder().decode(Deno.readFileSync(srcFile));
+
+ assertEquals(existsSync(srcFile), true);
+ assertEquals(existsSync(destFile), false);
+
+ copySync(srcFile, destFile);
+
+ assertEquals(existsSync(srcFile), true);
+ assertEquals(existsSync(destFile), true);
+
+ const destContent = new TextDecoder().decode(Deno.readFileSync(destFile));
+
+ assertEquals(srcContent, destContent);
+
+ // Copy again without overwrite option and it should throw an error.
+ assertThrows(
+ (): void => {
+ copySync(srcFile, destFile);
+ },
+ Error,
+ `'${destFile}' already exists.`
+ );
+
+ // Modify destination file.
+ Deno.writeFileSync(destFile, new TextEncoder().encode("txt copy"));
+
+ assertEquals(
+ new TextDecoder().decode(Deno.readFileSync(destFile)),
+ "txt copy"
+ );
+
+ // Copy again with overwrite option.
+ copySync(srcFile, destFile, { overwrite: true });
+
+ // Make sure the file has been overwritten.
+ assertEquals(new TextDecoder().decode(Deno.readFileSync(destFile)), "txt");
+ }
+);
+
+testCopySync(
+ "[fs] copy directory synchronously to its subdirectory",
+ (tempDir: string): void => {
+ const srcDir = path.join(tempDir, "parent");
+ const destDir = path.join(srcDir, "child");
+
+ ensureDirSync(srcDir);
+
+ assertThrows(
+ (): void => {
+ copySync(srcDir, destDir);
+ },
+ Error,
+ `Cannot copy '${srcDir}' to a subdirectory of itself, '${destDir}'.`
+ );
+ }
+);
+
+testCopySync(
+ "[fs] copy directory synchronously, and destination exist and not a directory",
+ (tempDir: string): void => {
+ const srcDir = path.join(tempDir, "parent_sync");
+ const destDir = path.join(tempDir, "child.txt");
+
+ ensureDirSync(srcDir);
+ ensureFileSync(destDir);
+
+ assertThrows(
+ (): void => {
+ copySync(srcDir, destDir);
+ },
+ Error,
+ `Cannot overwrite non-directory '${destDir}' with directory '${srcDir}'.`
+ );
+ }
+);
+
+testCopySync(
+ "[fs] copy directory synchronously",
+ (tempDir: string): void => {
+ const srcDir = path.join(testdataDir, "copy_dir");
+ const destDir = path.join(tempDir, "copy_dir_copy_sync");
+ const srcFile = path.join(srcDir, "0.txt");
+ const destFile = path.join(destDir, "0.txt");
+ const srcNestFile = path.join(srcDir, "nest", "0.txt");
+ const destNestFile = path.join(destDir, "nest", "0.txt");
+
+ copySync(srcDir, destDir);
+
+ assertEquals(existsSync(destFile), true);
+ assertEquals(existsSync(destNestFile), true);
+
+ // After copy. The source and destination should have the same content.
+ assertEquals(
+ new TextDecoder().decode(Deno.readFileSync(srcFile)),
+ new TextDecoder().decode(Deno.readFileSync(destFile))
+ );
+ assertEquals(
+ new TextDecoder().decode(Deno.readFileSync(srcNestFile)),
+ new TextDecoder().decode(Deno.readFileSync(destNestFile))
+ );
+
+ // Copy again without overwrite option and it should throw an error.
+ assertThrows(
+ (): void => {
+ copySync(srcDir, destDir);
+ },
+ Error,
+ `'${destDir}' already exists.`
+ );
+
+ // Modify the file in the destination directory.
+ Deno.writeFileSync(destNestFile, new TextEncoder().encode("nest copy"));
+ assertEquals(
+ new TextDecoder().decode(Deno.readFileSync(destNestFile)),
+ "nest copy"
+ );
+
+ // Copy again with overwrite option.
+ copySync(srcDir, destDir, { overwrite: true });
+
+ // Make sure the file has been overwritten.
+ assertEquals(
+ new TextDecoder().decode(Deno.readFileSync(destNestFile)),
+ "nest"
+ );
+ }
+);
+
+testCopySync(
+ "[fs] copy symlink file synchronously",
+ (tempDir: string): void => {
+ const dir = path.join(testdataDir, "copy_dir_link_file");
+ const srcLink = path.join(dir, "0.txt");
+ const destLink = path.join(tempDir, "0_copy.txt");
+
+ if (isWindows) {
+ assertThrows(
+ // (): void => copySync(srcLink, destLink),
+ (): void => ensureSymlinkSync(srcLink, destLink)
+ );
+ return;
+ }
+
+ assert(
+ Deno.lstatSync(srcLink).isSymlink(),
+ `'${srcLink}' should be symlink type`
+ );
+
+ copySync(srcLink, destLink);
+
+ const statInfo = Deno.lstatSync(destLink);
+
+ assert(statInfo.isSymlink(), `'${destLink}' should be symlink type`);
+ }
+);
+
+testCopySync(
+ "[fs] copy symlink directory synchronously",
+ (tempDir: string): void => {
+ const originDir = path.join(testdataDir, "copy_dir");
+ const srcLink = path.join(tempDir, "copy_dir_link");
+ const destLink = path.join(tempDir, "copy_dir_link_copy");
+
+ if (isWindows) {
+ assertThrows(
+ // (): void => copySync(srcLink, destLink),
+ (): void => ensureSymlinkSync(srcLink, destLink)
+ );
+ return;
+ }
+
+ ensureSymlinkSync(originDir, srcLink);
+
+ assert(
+ Deno.lstatSync(srcLink).isSymlink(),
+ `'${srcLink}' should be symlink type`
+ );
+
+ copySync(srcLink, destLink);
+
+ const statInfo = Deno.lstatSync(destLink);
+
+ assert(statInfo.isSymlink());
+ }
+);
diff --git a/fs/mod.ts b/fs/mod.ts
index 8635911a4..edbd7009f 100644
--- a/fs/mod.ts
+++ b/fs/mod.ts
@@ -8,6 +8,7 @@ export * from "./exists.ts";
export * from "./glob.ts";
export * from "./globrex.ts";
export * from "./move.ts";
+export * from "./copy.ts";
export * from "./read_file_str.ts";
export * from "./write_file_str.ts";
export * from "./read_json.ts";
diff --git a/fs/test.ts b/fs/test.ts
index 90e3c4688..43d6550b8 100644
--- a/fs/test.ts
+++ b/fs/test.ts
@@ -11,6 +11,7 @@ import "./ensure_file_test.ts";
import "./ensure_symlink_test.ts";
import "./ensure_link_test.ts";
import "./move_test.ts";
+import "./copy_test.ts";
import "./read_json_test.ts";
import "./write_json_test.ts";
import "./read_file_str_test.ts";
diff --git a/fs/testdata/copy_dir/0.txt b/fs/testdata/copy_dir/0.txt
new file mode 100644
index 000000000..f3a34851d
--- /dev/null
+++ b/fs/testdata/copy_dir/0.txt
@@ -0,0 +1 @@
+text \ No newline at end of file
diff --git a/fs/testdata/copy_dir/nest/0.txt b/fs/testdata/copy_dir/nest/0.txt
new file mode 100644
index 000000000..cd1b98cb3
--- /dev/null
+++ b/fs/testdata/copy_dir/nest/0.txt
@@ -0,0 +1 @@
+nest \ No newline at end of file
diff --git a/fs/testdata/copy_dir_link_file/0.txt b/fs/testdata/copy_dir_link_file/0.txt
new file mode 120000
index 000000000..63413ea1c
--- /dev/null
+++ b/fs/testdata/copy_dir_link_file/0.txt
@@ -0,0 +1 @@
+./fs/testdata/copy_dir/0.txt \ No newline at end of file
diff --git a/fs/testdata/copy_file.txt b/fs/testdata/copy_file.txt
new file mode 100644
index 000000000..84c22fd8a
--- /dev/null
+++ b/fs/testdata/copy_file.txt
@@ -0,0 +1 @@
+txt \ No newline at end of file