diff --git a/fs/README.md b/fs/README.md index 01780892df..8f4c472a86 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 0000000000..83c4e73ad0 --- /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 { + let destStat: Deno.FileInfo; + + destStat = await Deno.lstat(dest).catch( + (): Promise => 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 { + 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 { + 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 { + 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 { + 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 0000000000..03d439ec18 --- /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 +): Promise { + test({ + name, + async fn(): Promise { + 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 => { + 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 => { + await copy(srcFile, destFile); + } + ); + } +); + +testCopy( + "[fs] copy if src and dest are the same paths", + async (tempDir: string): Promise => { + const srcFile = path.join(tempDir, "copy_file_same.txt"); + const destFile = path.join(tempDir, "copy_file_same.txt"); + await assertThrowsAsync( + async (): Promise => { + await copy(srcFile, destFile); + }, + Error, + "Source and destination cannot be the same." + ); + } +); + +testCopy( + "[fs] copy file", + async (tempDir: string): Promise => { + 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 => { + 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 => { + 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 => { + const srcDir = path.join(tempDir, "parent"); + const destDir = path.join(srcDir, "child"); + + await ensureDir(srcDir); + + await assertThrowsAsync( + async (): Promise => { + 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 => { + const srcDir = path.join(tempDir, "parent"); + const destDir = path.join(tempDir, "child.txt"); + + await ensureDir(srcDir); + await ensureFile(destDir); + + await assertThrowsAsync( + async (): Promise => { + await copy(srcDir, destDir); + }, + Error, + `Cannot overwrite non-directory '${destDir}' with directory '${srcDir}'.` + ); + } +); + +testCopy( + "[fs] copy directory", + async (tempDir: string): Promise => { + 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 => { + 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 => { + 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 => copy(srcLink, destLink), + (): Promise => 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 => { + 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 => copy(srcLink, destLink), + (): Promise => 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 8635911a48..edbd7009f3 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 90e3c4688d..43d6550b89 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 0000000000..f3a34851d4 --- /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 0000000000..cd1b98cb37 --- /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 0000000000..63413ea1c1 --- /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 0000000000..84c22fd8a1 --- /dev/null +++ b/fs/testdata/copy_file.txt @@ -0,0 +1 @@ +txt \ No newline at end of file