All files / src/fs/operations directory-operations.js

99.02% Statements 101/102
97.3% Branches 72/74
100% Functions 20/20
98.77% Lines 80/81

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 2781x 1x 1x 1x 1x 1x   133x 157x                                                             1x 138x   138x 276x 708x   414x 306x 306x                     1x 174x                 1x 20x 1x         19x 3x         16x   16x                     23x 23x 1x         22x 3x         19x 19x       19x 13x 12x       6x                       15x 15x 15x   15x 3x         12x                         12x 12x 1x         11x                           36x 66x 22x 7x   2x 5x   1x         9x                             14x 14x 1x         13x 1x         12x 12x 12x 22x   12x 3x         9x 27x 57x 19x 18x                           14x 14x 1x         13x 2x         11x                   1x 1x   1x       1x    
import * as FileUtil from 'fs/util/file-util';
import * as GlobUtil from 'fs/util/glob-util';
import * as PathUtil from 'fs/util/path-util';
import * as BaseOp from 'fs/operations/base-operations';
import { makeError, fsErrorType } from 'fs/fs-error';
import { hasFile } from 'fs/operations/file-operations';
 
const onlyFilesFilter = fs => path => FileUtil.isFile(fs.get(path));
const onlyDirectoriesFilter = fs => path => FileUtil.isDirectory(fs.get(path));
 
/**
 * Fill file system gaps with empty directories.
 *
 * EXPLANATION:
 * A file system can be left in a state where there the directory structure
 * is incomplete and there may be illogical gaps in the structure after
 * manually creating or editing the file system.
 *
 * For example, we might have a file system that looks like this after manually
 * adding a directory of '/a/b/c':
 *
 * {
 *  '/': {..}
 *  '/a/b/c': {..}
 * }
 *
 * As a result of the operation, we're missing directories of '/a' and '/a/b'.
 * We can fill these missing directory gaps to get a properly formed directory
 * structure:
 *
 * {
 *  '/': {..}
 *  '/a': {..}
 *  '/a/b': {..}
 *  '/a/b/c': {..}
 * }
 * @param  {Map}    fs   file system with gaps in directory structure
 * @return {Map}         file system without directory gaps
 */
export const fillGaps = (fs) => {
  const emptyDirectory = FileUtil.makeDirectory();
 
  const directoryGapPaths = fs.keySeq() // sequence of paths
    .flatMap(path => PathUtil.getPathBreadCrumbs(path))
    .filter(path => !fs.has(path));
 
  return fs.withMutations((fs) => {
    for (const directoryGapPath of directoryGapPaths) {
      fs.set(directoryGapPath, emptyDirectory);
    }
  });
};
 
/**
 * Check if a directory exists in the file system
 * @param  {Map}     fs   file system
 * @param  {string}  path path to check if is a directory
 * @return {boolean}      true, if the directory exists
 */
export const hasDirectory = (fs, path) => {
  return fs.has(path) && FileUtil.isDirectory(fs.get(path));
};
 
/**
 * Creates a list of file names
 * @param  {Map}    fs   file system
 * @param  {string} path directory path to list files in
 * @return {object}      list of file names or an error
 */
export const listDirectoryFiles = (fs, path) => {
  if (hasFile(fs, path)) {
    return {
      err: makeError(fsErrorType.FILE_EXISTS, 'File exists at path')
    };
  }
 
  if (!hasDirectory(fs, path)) {
    return {
      err: makeError(fsErrorType.NO_SUCH_DIRECTORY, 'Cannot list files in non-existent directory')
    };
  };
 
  const filesPattern = path === '/' ? '/*' : `${path}/*`;
 
  return {
    list: GlobUtil.captureGlobPaths(fs, filesPattern, onlyFilesFilter(fs))
  };
};
 
/**
 * Creates a list of folder names inside the current directory.
 * @param  {Map}    fs   file system
 * @param  {string} path path to list directories in
 * @return {object}      list of directories or an error
 */
export const listDirectoryFolders = (fs, path, isTrailingSlashAppended = true) => {
  if (hasFile(fs, path)) {
    return {
      err: makeError(fsErrorType.FILE_EXISTS, 'File exists at path')
    };
  }
 
  if (!hasDirectory(fs, path)) {
    return {
      err: makeError(fsErrorType.NO_SUCH_DIRECTORY, 'Cannot list folders in non-existent directory')
    };
  };
 
  const foldersPattern = path === '/' ? '/*' : `${path}/*`;
  const folderNames = GlobUtil.captureGlobPaths(
    fs, foldersPattern, onlyDirectoriesFilter(fs)
  );
 
  if (isTrailingSlashAppended) {
    return {
      list: folderNames.map(folderName => `${folderName}/`)
    };
  }
 
  return {
    list: folderNames
  };
};
 
/**
 * Lists files and folders in a directory
 * @param  {Map}     fs                                      file system
 * @param  {string}  path                                    directory path to list files and folders in
 * @param  {boolean} [addTrailingSlash=true]                 add a / to the end of folder names
 * @return {object}                                          file system or an error
 */
export const listDirectory = (fs, path, addTrailingSlash = true) => {
  const {err: listFileErr, list: fileList} = listDirectoryFiles(fs, path);
  const {err: listFolderErr, list: folderList} = listDirectoryFolders(fs, path, addTrailingSlash);
 
  if (listFileErr || listFolderErr) {
    return {
      err: listFileErr ? listFileErr : listFolderErr
    };
  };
 
  return {
    list: fileList.concat(folderList)
  };
};
 
/**
 * Adds a directory to the file system
 * @param {Map}     fs                           file system
 * @param {string}  path                         path to add a directory to
 * @param {Map}     dir                          directory
 * @param {boolean} [isReplaceExistingDir=false] whether a directory can be overwritten if it already exists
 * @return {object}                              file system or an error
 */
export const addDirectory = (fs, path, dir, addParentPaths = true) => {
  if (hasFile(fs, PathUtil.getPathParent(path))) {
    return {
      err: makeError(fsErrorType.FILE_EXISTS, 'File exists at path')
    };
  }
 
  return BaseOp.add(fs, path, dir, addParentPaths);
};
 
/**
 * Private helper function implementing rules for replacing a source path (the path
 * we're copying from) with a destination path (the path we're copying to). Note
 * that in our file system:
 * - A file cannot overwrite a directory,
 * - a directory cannot overwrite a file, and
 * - a file/directory can overwrite a file/directory.
 * @param  {Map}       fs      file system
 * @param  {Sequence}  pathSeq sequence of source and destination paths
 * @return {Boolean}           true, if a source path can replace a destination path
 */
const isPathTypeMatching = (fs, pathSeq) => {
  for (const [srcPath, destPath] of pathSeq) {
    if (fs.has(destPath)) {
      if (hasFile(fs, srcPath) && hasDirectory(fs, destPath)) {
        // Cannot overwrite a file with a directory
        return false;
      } else if (hasDirectory(fs, srcPath) && hasFile(fs, destPath)) {
        // Cannot overwrite a directory with a file
        return false;
      }
    }
  }
 
  return true;
};
 
/**
 * Copies a directory (and all directories included inside that directory)
 * from a source directory to a destination directory
 *
 * If the destination doesn't exist, it can be created.
 *
 * The source and destination must be a directory and not a file.
 * @param  {Map}     fs                             file system
 * @param  {string}  srcPath                        directory path to copy from
 * @param  {string}  destPath                       directory path to copy to
 * @return {object}                                 file system or an error
 */
export const copyDirectory = (fs, srcPath, destPath, overwrite = true) => {
  if (!hasDirectory(fs, srcPath)) {
    return {
      err: makeError(fsErrorType.NO_SUCH_DIRECTORY, 'Source directory does not exist')
    };
  };
 
  if (!hasDirectory(fs, destPath)) {
    return {
      err: makeError(fsErrorType.NO_SUCH_DIRECTORY, 'Destination directory does not exist')
    };
  };
 
  const srcChildPattern = srcPath === '/' ? '/**' : `${srcPath}/**`;
  const srcPaths = GlobUtil.globPaths(fs, srcChildPattern);
  const srcSubPaths = GlobUtil.captureGlobPaths(fs, srcChildPattern);
  const destPaths = srcSubPaths.map(path => path === '/' ? destPath : `${destPath}/${path}`);
 
  if (!isPathTypeMatching(fs, srcPaths.zip(destPaths))) {
    return {
      err: makeError(fsErrorType.OTHER, 'Cannot overwrite a directory with file OR a file with directory')
    };
  }
 
  return {
    fs: fs.withMutations((newFs) => {
      for (const [srcPath, destPath] of srcPaths.zip(destPaths)) {
        if (!fs.has(destPath) || overwrite) {
          newFs.set(destPath, fs.get(srcPath));
        }
      }
    })
  };
};
 
/**
 * Remove a directory from a file system
 * @param  {Map}     fs                                   file system
 * @param  {string}  pathToDelete                         directory path to delete
 * @param  {Boolean} [isNonEmptyDirectoryRemovable=false] whether directories with files in them can be removed
 * @return {object}                                       file system or an error
 */
export const deleteDirectory = (fs, pathToDelete, isNonEmptyDirectoryRemovable = false) => {
  if (hasFile(fs, pathToDelete)) {
    return {
      err: makeError(fsErrorType.FILE_EXISTS, 'File exists at path')
    };
  }
 
  if (!hasDirectory(fs, pathToDelete)) {
    return {
      err: makeError(fsErrorType.NO_SUCH_DIRECTORY, `No such directory: ${pathToDelete}`)
    };
  };
 
  return BaseOp.remove(fs, pathToDelete, isNonEmptyDirectoryRemovable);
};
 
/**
 * Rename a directory
 * @param  {Map}    fs          file system
 * @param  {string} currentPath directory path to rename (and hence remove)
 * @param  {string} newPath     path to place the renamed directory
 * @return {object}             file system or an error
 */
export const renameDirectory = (fs, currentPath, newPath) => {
  const {err, fs: copiedFS} = copyDirectory(fs, currentPath, newPath, true);
 
  Iif (err) {
    return {err};
  }
 
  return deleteDirectory(copiedFS, currentPath, true);
};