file.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. /*
  2. * Utilities: A classic collection of JavaScript utilities
  3. * Copyright 2112 Matthew Eernisse ([email protected])
  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. var fs = require('fs')
  19. , path = require('path')
  20. , DEFAULT_INCLUDE_PAT = /\.(js|coffee|css|less|scss)$/
  21. , DEFAULT_EXCLUDE_PAT = /\.git|node_modules/
  22. , logger;
  23. var logger = new (function () {
  24. var out;
  25. try {
  26. out = require('./log');
  27. }
  28. catch (e) {
  29. out = console;
  30. }
  31. this.log = function (o) {
  32. out.log(o);
  33. };
  34. })();
  35. /**
  36. @name file
  37. @namespace file
  38. */
  39. var fileUtils = new (function () {
  40. var _copyFile
  41. , _copyDir
  42. , _readDir
  43. , _rmDir
  44. , _watch;
  45. // Recursively copy files and directories
  46. _copyFile = function (fromPath, toPath, opts) {
  47. var from = path.normalize(fromPath)
  48. , to = path.normalize(toPath)
  49. , options = opts || {}
  50. , fromStat
  51. , toStat
  52. , destExists
  53. , destDoesNotExistErr
  54. , content
  55. , filename
  56. , dirContents
  57. , targetDir;
  58. fromStat = fs.statSync(from);
  59. try {
  60. //console.dir(to + ' destExists');
  61. toStat = fs.statSync(to);
  62. destExists = true;
  63. }
  64. catch(e) {
  65. //console.dir(to + ' does not exist');
  66. destDoesNotExistErr = e;
  67. destExists = false;
  68. }
  69. // Destination dir or file exists, copy into (directory)
  70. // or overwrite (file)
  71. if (destExists) {
  72. // If there's a rename-via-copy file/dir name passed, use it.
  73. // Otherwise use the actual file/dir name
  74. filename = options.rename || path.basename(from);
  75. // Copying a directory
  76. if (fromStat.isDirectory()) {
  77. dirContents = fs.readdirSync(from);
  78. targetDir = path.join(to, filename);
  79. // We don't care if the target dir already exists
  80. try {
  81. fs.mkdirSync(targetDir, options.mode || parseInt(755, 8));
  82. }
  83. catch(e) {
  84. if (e.code != 'EEXIST') {
  85. throw e;
  86. }
  87. }
  88. for (var i = 0, ii = dirContents.length; i < ii; i++) {
  89. //console.log(dirContents[i]);
  90. _copyFile(path.join(from, dirContents[i]), targetDir);
  91. }
  92. }
  93. // Copying a file
  94. else {
  95. content = fs.readFileSync(from);
  96. // Copy into dir
  97. if (toStat.isDirectory()) {
  98. //console.log('copy into dir ' + to);
  99. fs.writeFileSync(path.join(to, filename), content);
  100. }
  101. // Overwrite file
  102. else {
  103. //console.log('overwriting ' + to);
  104. fs.writeFileSync(to, content);
  105. }
  106. }
  107. }
  108. // Dest doesn't exist, can't create it
  109. else {
  110. throw destDoesNotExistErr;
  111. }
  112. };
  113. _copyDir = function (from, to, opts) {
  114. var createDir = opts.createDir;
  115. };
  116. // Return the contents of a given directory
  117. _readDir = function (dirPath) {
  118. var dir = path.normalize(dirPath)
  119. , paths = []
  120. , ret = [dir]
  121. , msg;
  122. try {
  123. paths = fs.readdirSync(dir);
  124. }
  125. catch (e) {
  126. msg = 'Could not read path ' + dir + '\n';
  127. if (e.stack) {
  128. msg += e.stack;
  129. }
  130. throw new Error(msg);
  131. }
  132. paths.forEach(function (p) {
  133. var curr = path.join(dir, p);
  134. var stat = fs.statSync(curr);
  135. if (stat.isDirectory()) {
  136. ret = ret.concat(_readDir(curr));
  137. }
  138. else {
  139. ret.push(curr);
  140. }
  141. });
  142. return ret;
  143. };
  144. // Remove the given directory
  145. _rmDir = function (dirPath) {
  146. var dir = path.normalize(dirPath)
  147. , paths = [];
  148. paths = fs.readdirSync(dir);
  149. paths.forEach(function (p) {
  150. var curr = path.join(dir, p);
  151. var stat = fs.statSync(curr);
  152. if (stat.isDirectory()) {
  153. _rmDir(curr);
  154. }
  155. else {
  156. try {
  157. fs.unlinkSync(curr);
  158. } catch(e) {
  159. if (e.code === 'EPERM') {
  160. fs.chmodSync(curr, parseInt(666, 8));
  161. fs.unlinkSync(curr);
  162. } else {
  163. throw e;
  164. }
  165. }
  166. }
  167. });
  168. fs.rmdirSync(dir);
  169. };
  170. // Recursively watch files with a callback
  171. _watch = function () {
  172. var args = Array.prototype.slice.call(arguments)
  173. , filePath
  174. , opts
  175. , callback
  176. , inclPat
  177. , exclPat
  178. , createWatcher;
  179. filePath = args.shift();
  180. callback = args.pop();
  181. opts = args.pop() || {};
  182. inclPat = opts.includePattern || DEFAULT_INCLUDE_PAT;
  183. exclPat = opts.excludePattern || DEFAULT_EXCLUDE_PAT;
  184. opts.level = opts.level || 1;
  185. createWatcher = function (watchPath) {
  186. if (!exclPat.test(watchPath)) {
  187. fs.watch(watchPath, function (ev, p) {
  188. if (inclPat.test(p) && !exclPat.test(p)) {
  189. callback(path.join(watchPath, p));
  190. }
  191. });
  192. }
  193. };
  194. fs.stat(filePath, function (err, stats) {
  195. if (err) {
  196. return false;
  197. }
  198. // Watch files at the top level
  199. if (stats.isFile() && opts.level == 1) {
  200. createWatcher(filePath);
  201. opts.level++;
  202. }
  203. else if (stats.isDirectory()) {
  204. createWatcher(filePath);
  205. opts.level++;
  206. fs.readdir(filePath, function (err, files) {
  207. if (err) {
  208. return log.fatal(err);
  209. }
  210. for (var f in files) {
  211. _watch(path.join(filePath, files[f]), opts, callback);
  212. }
  213. });
  214. }
  215. });
  216. };
  217. /**
  218. @name file#cpR
  219. @public
  220. @function
  221. @description Copies a directory/file to a destination
  222. @param {String} fromPath The source path to copy from
  223. @param {String} toPath The destination path to copy to
  224. @param {Object} opts Options to use
  225. @param {Boolean} [opts.silent] If false then will log the command
  226. */
  227. this.cpR = function (fromPath, toPath, options) {
  228. var from = path.normalize(fromPath)
  229. , to = path.normalize(toPath)
  230. , toStat
  231. , doesNotExistErr
  232. , paths
  233. , filename
  234. , opts = options || {};
  235. if (!opts.silent) {
  236. logger.log('cp -r ' + fromPath + ' ' + toPath);
  237. }
  238. if (from == to) {
  239. throw new Error('Cannot copy ' + from + ' to itself.');
  240. }
  241. // Handle rename-via-copy
  242. try {
  243. toStat = fs.statSync(to);
  244. }
  245. catch(e) {
  246. doesNotExistErr = e;
  247. // Get abs path so it's possible to check parent dir
  248. if (!this.isAbsolute(to)) {
  249. to = path.join(process.cwd() , to);
  250. }
  251. // Save the file/dir name
  252. filename = path.basename(to);
  253. // See if a parent dir exists, so there's a place to put the
  254. /// renamed file/dir (resets the destination for the copy)
  255. to = path.dirname(to);
  256. try {
  257. toStat = fs.statSync(to);
  258. }
  259. catch(e) {}
  260. if (toStat && toStat.isDirectory()) {
  261. // Set the rename opt to pass to the copy func, will be used
  262. // as the new file/dir name
  263. opts.rename = filename;
  264. //console.log('filename ' + filename);
  265. }
  266. else {
  267. throw doesNotExistErr;
  268. }
  269. }
  270. _copyFile(from, to, opts);
  271. };
  272. /**
  273. @name file#mkdirP
  274. @public
  275. @function
  276. @description Create the given directory(ies) using the given mode permissions
  277. @param {String} dir The directory to create
  278. @param {Number} mode The mode to give the created directory(ies)(Default: 0755)
  279. */
  280. this.mkdirP = function (dir, mode) {
  281. var dirPath = path.normalize(dir)
  282. , paths = dirPath.split(/\/|\\/)
  283. , currPath = ''
  284. , next;
  285. if (paths[0] == '' || /^[A-Za-z]+:/.test(paths[0])) {
  286. currPath = paths.shift() || '/';
  287. currPath = path.join(currPath, paths.shift());
  288. //console.log('basedir');
  289. }
  290. while ((next = paths.shift())) {
  291. if (next == '..') {
  292. currPath = path.join(currPath, next);
  293. continue;
  294. }
  295. currPath = path.join(currPath, next);
  296. try {
  297. //console.log('making ' + currPath);
  298. fs.mkdirSync(currPath, mode || parseInt(755, 8));
  299. }
  300. catch(e) {
  301. if (e.code != 'EEXIST') {
  302. throw e;
  303. }
  304. }
  305. }
  306. };
  307. /**
  308. @name file#readdirR
  309. @public
  310. @function
  311. @return {Array} Returns the contents as an Array, can be configured via opts.format
  312. @description Reads the given directory returning it's contents
  313. @param {String} dir The directory to read
  314. @param {Object} opts Options to use
  315. @param {String} [opts.format] Set the format to return(Default: Array)
  316. */
  317. this.readdirR = function (dir, opts) {
  318. var options = opts || {}
  319. , format = options.format || 'array'
  320. , ret;
  321. ret = _readDir(dir);
  322. return format == 'string' ? ret.join('\n') : ret;
  323. };
  324. /**
  325. @name file#rmRf
  326. @public
  327. @function
  328. @description Deletes the given directory/file
  329. @param {String} p The path to delete, can be a directory or file
  330. @param {Object} opts Options to use
  331. @param {String} [opts.silent] If false then logs the command
  332. */
  333. this.rmRf = function (p, options) {
  334. var stat
  335. , opts = options || {};
  336. if (!opts.silent) {
  337. logger.log('rm -rf ' + p);
  338. }
  339. try {
  340. stat = fs.statSync(p);
  341. if (stat.isDirectory()) {
  342. _rmDir(p);
  343. }
  344. else {
  345. fs.unlinkSync(p);
  346. }
  347. }
  348. catch (e) {}
  349. };
  350. /**
  351. @name file#isAbsolute
  352. @public
  353. @function
  354. @return {Boolean/String} If it's absolute the first character is returned otherwise false
  355. @description Checks if a given path is absolute or relative
  356. @param {String} p Path to check
  357. */
  358. this.isAbsolute = function (p) {
  359. var match = /^[A-Za-z]+:\\|^\//.exec(p);
  360. if (match && match.length) {
  361. return match[0];
  362. }
  363. return false;
  364. };
  365. /**
  366. @name file#absolutize
  367. @public
  368. @function
  369. @return {String} Returns the absolute path for the given path
  370. @description Returns the absolute path for the given path
  371. @param {String} p The path to get the absolute path for
  372. */
  373. this.absolutize = function (p) {
  374. if (this.isAbsolute(p)) {
  375. return p;
  376. }
  377. else {
  378. return path.join(process.cwd(), p);
  379. }
  380. };
  381. /**
  382. Given a patern, return the base directory of it (ie. the folder
  383. that will contain all the files matching the path).
  384. eg. file.basedir('/test/**') => '/test/'
  385. Path ending by '/' are considerd as folder while other are considerd
  386. as files, eg.:
  387. file.basedir('/test/a/') => '/test/a'
  388. file.basedir('/test/a') => '/test'
  389. The returned path always end with a '/' so we have:
  390. file.basedir(file.basedir(x)) == file.basedir(x)
  391. */
  392. this.basedir = function (pathParam) {
  393. var basedir = ''
  394. , parts
  395. , part
  396. , pos = 0
  397. , p = pathParam || '';
  398. // If the path has a leading asterisk, basedir is the current dir
  399. if (p.indexOf('*') == 0 || p.indexOf('**') == 0) {
  400. return '.';
  401. }
  402. // always consider .. at the end as a folder and not a filename
  403. if (/(?:^|\/|\\)\.\.$/.test(p.slice(-3))) {
  404. p += '/';
  405. }
  406. parts = p.split(/\\|\//);
  407. for (var i = 0, l = parts.length - 1; i < l; i++) {
  408. part = parts[i];
  409. if (part.indexOf('*') > -1 || part.indexOf('**') > -1) {
  410. break;
  411. }
  412. pos += part.length + 1;
  413. basedir += part + p[pos - 1];
  414. }
  415. if (!basedir) {
  416. basedir = '.';
  417. }
  418. // Strip trailing slashes
  419. if (!(basedir == '\\' || basedir == '/')) {
  420. basedir = basedir.replace(/\\$|\/$/, '');
  421. }
  422. return basedir;
  423. };
  424. /**
  425. @name file#searchParentPath
  426. @public
  427. @function
  428. @description Search for a directory/file in the current directory and parent directories
  429. @param {String} p The path to search for
  430. @param {Function} callback The function to call once the path is found
  431. */
  432. this.searchParentPath = function (location, beginPath, callback) {
  433. if (typeof beginPath === 'function' && !callback) {
  434. callback = beginPath;
  435. beginPath = process.cwd();
  436. }
  437. var cwd = beginPath || process.cwd();
  438. if (!location) {
  439. // Return if no path is given
  440. return;
  441. }
  442. var relPath = ''
  443. , i = 5 // Only search up to 5 directories
  444. , pathLoc
  445. , pathExists;
  446. while (--i >= 0) {
  447. pathLoc = path.join(cwd, relPath, location);
  448. pathExists = this.existsSync(pathLoc);
  449. if (pathExists) {
  450. callback && callback(undefined, pathLoc);
  451. break;
  452. } else {
  453. // Dir could not be found
  454. if (i === 0) {
  455. callback && callback(new Error("Path \"" + pathLoc + "\" not found"), undefined);
  456. break;
  457. }
  458. // Add a relative parent directory
  459. relPath += '../';
  460. // Switch to relative parent directory
  461. process.chdir(path.join(cwd, relPath));
  462. }
  463. }
  464. };
  465. /**
  466. @name file#watch
  467. @public
  468. @function
  469. @description Watch a given path then calls the callback once a change occurs
  470. @param {String} path The path to watch
  471. @param {Function} callback The function to call when a change occurs
  472. */
  473. this.watch = function () {
  474. _watch.apply(this, arguments);
  475. };
  476. // Compatibility for fs.exists(0.8) and path.exists(0.6)
  477. this.exists = (typeof fs.exists === 'function') ? fs.exists : path.exists;
  478. // Compatibility for fs.existsSync(0.8) and path.existsSync(0.6)
  479. this.existsSync = (typeof fs.existsSync === 'function') ? fs.existsSync : path.existsSync;
  480. /**
  481. @name file#requireLocal
  482. @public
  483. @function
  484. @return {Object} The given module is returned
  485. @description Require a local module from the node_modules in the current directory
  486. @param {String} module The module to require
  487. @param {String} message An option message to throw if the module doesn't exist
  488. */
  489. this.requireLocal = function (module, message) {
  490. var dep;
  491. // Try to require in the application directory
  492. try {
  493. dep = require(path.join(process.cwd(), 'node_modules', module));
  494. }
  495. catch(err) {
  496. if (message) {
  497. throw new Error(message);
  498. }
  499. throw new Error('Module "' + module + '" could not be found as a ' +
  500. 'local module. Please install it by doing "npm install ' +
  501. module + '"');
  502. }
  503. return dep;
  504. };
  505. })();
  506. module.exports = fileUtils;