1/*
2 * Utilities: A classic collection of JavaScript utilities
3 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *         http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17*/
18
19let fs = require('fs');
20let path = require('path');
21
22/**
23  @name file
24  @namespace file
25*/
26
27let fileUtils = new (function () {
28
29  // Recursively copy files and directories
30  let _copyFile = function (fromPath, toPath, opts) {
31    let from = path.normalize(fromPath);
32    let to = path.normalize(toPath);
33    let options = opts || {};
34    let fromStat;
35    let toStat;
36    let destExists;
37    let destDoesNotExistErr;
38    let content;
39    let filename;
40    let dirContents;
41    let targetDir;
42
43    fromStat = fs.statSync(from);
44
45    try {
46      //console.dir(to + ' destExists');
47      toStat = fs.statSync(to);
48      destExists = true;
49    }
50    catch(e) {
51      //console.dir(to + ' does not exist');
52      destDoesNotExistErr = e;
53      destExists = false;
54    }
55    // Destination dir or file exists, copy into (directory)
56    // or overwrite (file)
57    if (destExists) {
58
59      // If there's a rename-via-copy file/dir name passed, use it.
60      // Otherwise use the actual file/dir name
61      filename = options.rename || path.basename(from);
62
63      // Copying a directory
64      if (fromStat.isDirectory()) {
65        dirContents = fs.readdirSync(from);
66        targetDir = path.join(to, filename);
67        // We don't care if the target dir already exists
68        try {
69          fs.mkdirSync(targetDir, {mode: fromStat.mode & 0o777});
70        }
71        catch(e) {
72          if (e.code !== 'EEXIST') {
73            throw e;
74          }
75        }
76        for (let i = 0, ii = dirContents.length; i < ii; i++) {
77          _copyFile(path.join(from, dirContents[i]), targetDir, {preserveMode: options.preserveMode});
78        }
79      }
80      // Copying a file
81      else {
82        content = fs.readFileSync(from);
83        let mode = fromStat.mode & 0o777;
84        let targetFile = to;
85
86        if (toStat.isDirectory()) {
87          targetFile = path.join(to, filename);
88        }
89
90        let fileExists = fs.existsSync(targetFile);
91        fs.writeFileSync(targetFile, content);
92
93        // If the file didn't already exist, use the original file mode.
94        // Otherwise, only update the mode if preserverMode is true.
95        if(!fileExists || options.preserveMode) {
96          fs.chmodSync(targetFile, mode);
97        }
98      }
99    }
100    // Dest doesn't exist, can't create it
101    else {
102      throw destDoesNotExistErr;
103    }
104  };
105
106  // Remove the given directory
107  let _rmDir = function (dirPath) {
108    let dir = path.normalize(dirPath);
109    let paths = [];
110    paths = fs.readdirSync(dir);
111    paths.forEach(function (p) {
112      let curr = path.join(dir, p);
113      let stat = fs.lstatSync(curr);
114      if (stat.isDirectory()) {
115        _rmDir(curr);
116      }
117      else {
118        try {
119          fs.unlinkSync(curr);
120        } catch(e) {
121          if (e.code === 'EPERM') {
122            fs.chmodSync(curr, parseInt(666, 8));
123            fs.unlinkSync(curr);
124          } else {
125            throw e;
126          }
127        }
128      }
129    });
130    fs.rmdirSync(dir);
131  };
132
133  /**
134    @name file#cpR
135    @public
136    @function
137    @description Copies a directory/file to a destination
138    @param {String} fromPath The source path to copy from
139    @param {String} toPath The destination path to copy to
140    @param {Object} opts Options to use
141      @param {Boolean} [opts.preserveMode] If target file already exists, this
142        determines whether the original file's mode is copied over. The default of
143        false mimics the behavior of the `cp` command line tool. (Default: false)
144  */
145  this.cpR = function (fromPath, toPath, options) {
146    let from = path.normalize(fromPath);
147    let to = path.normalize(toPath);
148    let toStat;
149    let doesNotExistErr;
150    let filename;
151    let opts = options || {};
152
153    if (from == to) {
154      throw new Error('Cannot copy ' + from + ' to itself.');
155    }
156
157    // Handle rename-via-copy
158    try {
159      toStat = fs.statSync(to);
160    }
161    catch(e) {
162      doesNotExistErr = e;
163
164      // Get abs path so it's possible to check parent dir
165      if (!this.isAbsolute(to)) {
166        to = path.join(process.cwd(), to);
167      }
168
169      // Save the file/dir name
170      filename = path.basename(to);
171      // See if a parent dir exists, so there's a place to put the
172      /// renamed file/dir (resets the destination for the copy)
173      to = path.dirname(to);
174      try {
175        toStat = fs.statSync(to);
176      }
177      catch(e) {}
178      if (toStat && toStat.isDirectory()) {
179        // Set the rename opt to pass to the copy func, will be used
180        // as the new file/dir name
181        opts.rename = filename;
182        //console.log('filename ' + filename);
183      }
184      else {
185        throw doesNotExistErr;
186      }
187    }
188
189    _copyFile(from, to, opts);
190  };
191
192  /**
193    @name file#mkdirP
194    @public
195    @function
196    @description Create the given directory(ies) using the given mode permissions
197    @param {String} dir The directory to create
198    @param {Number} mode The mode to give the created directory(ies)(Default: 0755)
199  */
200  this.mkdirP = function (dir, mode) {
201    let dirPath = path.normalize(dir);
202    let paths = dirPath.split(/\/|\\/);
203    let currPath = '';
204    let next;
205
206    if (paths[0] == '' || /^[A-Za-z]+:/.test(paths[0])) {
207      currPath = paths.shift() || '/';
208      currPath = path.join(currPath, paths.shift());
209      //console.log('basedir');
210    }
211    while ((next = paths.shift())) {
212      if (next == '..') {
213        currPath = path.join(currPath, next);
214        continue;
215      }
216      currPath = path.join(currPath, next);
217      try {
218        //console.log('making ' + currPath);
219        fs.mkdirSync(currPath, mode || parseInt(755, 8));
220      }
221      catch(e) {
222        if (e.code != 'EEXIST') {
223          throw e;
224        }
225      }
226    }
227  };
228
229  /**
230    @name file#rmRf
231    @public
232    @function
233    @description Deletes the given directory/file
234    @param {String} p The path to delete, can be a directory or file
235  */
236  this.rmRf = function (p, options) {
237    let stat;
238    try {
239      stat = fs.lstatSync(p);
240      if (stat.isDirectory()) {
241        _rmDir(p);
242      }
243      else {
244        fs.unlinkSync(p);
245      }
246    }
247    catch (e) {}
248  };
249
250  /**
251    @name file#isAbsolute
252    @public
253    @function
254    @return {Boolean/String} If it's absolute the first character is returned otherwise false
255    @description Checks if a given path is absolute or relative
256    @param {String} p Path to check
257  */
258  this.isAbsolute = function (p) {
259    let match = /^[A-Za-z]+:\\|^\//.exec(p);
260    if (match && match.length) {
261      return match[0];
262    }
263    return false;
264  };
265
266  /**
267    @name file#absolutize
268    @public
269    @function
270    @return {String} Returns the absolute path for the given path
271    @description Returns the absolute path for the given path
272    @param {String} p The path to get the absolute path for
273  */
274  this.absolutize = function (p) {
275    if (this.isAbsolute(p)) {
276      return p;
277    }
278    else {
279      return path.join(process.cwd(), p);
280    }
281  };
282
283})();
284
285module.exports = fileUtils;
286
287