index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /*
  2. Copyright (c) 2012, Yahoo! Inc. All rights reserved.
  3. Code licensed under the BSD License:
  4. http://yuilibrary.com/license/
  5. */
  6. var fs = require('graceful-fs');
  7. var Stack = require('./stack').Stack;
  8. var path = require('path');
  9. var rimraf = require('rimraf');
  10. var mkdirp = require('mkdirp');
  11. var getTree = function(from, options, callback) {
  12. var stack = new Stack(),
  13. errors = [],
  14. results = {};
  15. options.stats = options.stats || {};
  16. options.toHash = options.toHash || {};
  17. fs.readdir(from, stack.add(function(err, dirs) {
  18. if (!dirs.length) {
  19. results[from] = true;
  20. fs.stat(from, stack.add(function(err, stat) {
  21. /*istanbul ignore next*/
  22. if (err) {
  23. return errors.push(err);
  24. }
  25. options.stats[from] = stat;
  26. options.toHash[from] = path.join(options.to, path.relative(options.from, from));
  27. }));
  28. }
  29. dirs.forEach(function (dir) {
  30. var base = path.join(from, dir);
  31. fs.stat(base, stack.add(function(err, stat) {
  32. options.stats[base] = stat;
  33. options.toHash[base] = path.join(options.to, path.relative(options.from, base));
  34. /*istanbul ignore next*/
  35. if (err) {
  36. return errors.push(err);
  37. }
  38. if (stat.isDirectory()) {
  39. getTree(base, options, stack.add(function(errs, tree) {
  40. /*istanbul ignore next*/
  41. if (errs && errs.length) {
  42. errs.forEach(function(item) {
  43. errors.push(item);
  44. });
  45. }
  46. //tree is always an Array
  47. tree.forEach(function(item) {
  48. results[item] = true;
  49. });
  50. }));
  51. } else {
  52. results[base] = true;
  53. }
  54. }));
  55. });
  56. }));
  57. stack.done(function() {
  58. callback(errors, Object.keys(results).sort());
  59. });
  60. };
  61. var filterTree = function (tree, options, callback) {
  62. var t = tree;
  63. if (options.filter) {
  64. if (typeof options.filter === 'function') {
  65. t = tree.filter(options.filter);
  66. } else if (options.filter instanceof RegExp) {
  67. t = tree.filter(function(item) {
  68. return !options.filter.test(item);
  69. });
  70. }
  71. }
  72. callback(null, t);
  73. };
  74. var splitTree = function (tree, options, callback) {
  75. var files = {},
  76. dirs = {};
  77. tree.forEach(function(item) {
  78. var to = options.toHash[item];
  79. if (options.stats[item] && options.stats[item].isDirectory()) {
  80. dirs[item] = true;
  81. } else {
  82. dirs[path.dirname(item)] = true;
  83. options.stats[path.dirname(item)] = fs.statSync(path.dirname(item));
  84. options.toHash[path.dirname(item)] = path.dirname(to);
  85. }
  86. });
  87. tree.forEach(function(item) {
  88. if (!dirs[item]) {
  89. files[item] = true;
  90. }
  91. });
  92. callback(Object.keys(dirs).sort(), Object.keys(files).sort());
  93. };
  94. var createDirs = function(dirs, to, options, callback) {
  95. var stack = new Stack();
  96. dirs.forEach(function(dir) {
  97. var stat = options.stats[dir],
  98. to = options.toHash[dir];
  99. /*istanbul ignore else*/
  100. if (to && typeof to === 'string') {
  101. fs.stat(to, stack.add(function(err, s) {
  102. if (s && !s.isDirectory()) {
  103. /*istanbul ignore next*/
  104. err = new Error(to + ' exists and is not a directory, can not create');
  105. /*istanbul ignore next*/
  106. err.code = 'ENOTDIR';
  107. /*istanbul ignore next*/
  108. err.errno = 27;
  109. options.errors.push(err);
  110. } else {
  111. mkdirp(to, stat.mode, stack.add(function(err) {
  112. /*istanbul ignore next*/
  113. if (err) {
  114. options.errors.push(err);
  115. }
  116. }));
  117. }
  118. }));
  119. }
  120. });
  121. stack.done(function() {
  122. callback();
  123. });
  124. };
  125. var copyFile = function(from, to, options, callback) {
  126. var dir = path.dirname(to);
  127. mkdirp(dir, function() {
  128. fs.stat(to, function(statError) {
  129. var err;
  130. if(!statError && options.overwrite !== true) {
  131. /*istanbul ignore next*/
  132. err = new Error('File '+to+' exists');
  133. /*istanbul ignore next*/
  134. err.code = 'EEXIST';
  135. /*istanbul ignore next*/
  136. err.errno = 47;
  137. return callback(err);
  138. }
  139. var fromFile = fs.createReadStream(from),
  140. toFile = fs.createWriteStream(to, {
  141. mode: options.stats[from].mode
  142. }),
  143. called = false,
  144. cb = function(e) {
  145. /*istanbul ignore next - This catches a hard to trap race condition */
  146. if (!called) {
  147. callback(e);
  148. called = true;
  149. }
  150. },
  151. /*istanbul ignore next*/
  152. onError = function (e) {
  153. err = e;
  154. cb(e);
  155. };
  156. fromFile.on('error', onError);
  157. toFile.on('error', onError);
  158. toFile.once('finish', function() {
  159. cb(err);
  160. });
  161. fromFile.pipe(toFile);
  162. });
  163. });
  164. };
  165. var createFiles = function(files, to, options, callback) {
  166. var next = process.nextTick,
  167. complete = 0,
  168. count = files.length,
  169. check = function() {
  170. /*istanbul ignore else - Shouldn't need this if graceful-fs does it's job*/
  171. if (count === complete) {
  172. callback();
  173. }
  174. },
  175. copy = function() {
  176. var from = files.pop(),
  177. to = options.toHash[from],
  178. bail;
  179. if (!from) {
  180. return check();
  181. }
  182. copyFile(from, to, options, function(err) {
  183. /*istanbul ignore next*/
  184. //This shouldn't happen with graceful-fs, but just in case
  185. if (/EMFILE/.test(err)) {
  186. bail = true;
  187. files.push(from);
  188. } else if (err) {
  189. options.errors.push(err);
  190. }
  191. /*istanbul ignore next*/
  192. if (!bail) {
  193. complete++;
  194. }
  195. next(copy);
  196. });
  197. };
  198. copy();
  199. };
  200. var confirm = function(files, options, callback) {
  201. var stack = new Stack(),
  202. errors = [],
  203. f = [],
  204. filtered = files;
  205. if (options.filter) {
  206. if (typeof options.filter === 'function') {
  207. filtered = files.filter(options.filter);
  208. } else if (options.filter instanceof RegExp) {
  209. filtered = files.filter(function(item) {
  210. return !options.filter.test(item);
  211. });
  212. }
  213. }
  214. /*istanbul ignore else - filtered should be an array, but just in case*/
  215. if (filtered.length) {
  216. filtered.forEach(function(file) {
  217. fs.stat(file, stack.add(function(err, stat) {
  218. /*istanbul ignore next*/
  219. if (err) {
  220. errors.push(err);
  221. } else {
  222. if (stat && (stat.isFile() || stat.isDirectory())) {
  223. f.push(file);
  224. }
  225. }
  226. }));
  227. });
  228. }
  229. stack.done(function() {
  230. /*istanbul ignore next */
  231. callback(((errors.length) ? errors : null), f.sort());
  232. });
  233. };
  234. var cpr = function(from, to, opts, callback) {
  235. if (typeof opts === 'function') {
  236. callback = opts;
  237. opts = {};
  238. }
  239. var options = {},
  240. proc;
  241. /*istanbul ignore next - in case a callback isn't provided*/
  242. callback = callback || function () {};
  243. Object.keys(opts).forEach(function(key) {
  244. options[key] = opts[key];
  245. });
  246. options.from = from;
  247. options.to = to;
  248. options.errors = [];
  249. proc = function() {
  250. getTree(options.from, options, function(err, tree) {
  251. filterTree(tree, options, function(err, t) {
  252. splitTree(t, options, function(dirs, files) {
  253. if (!dirs.length && !files.length) {
  254. return callback(new Error('No files to copy'));
  255. }
  256. createDirs(dirs, to, options, function() {
  257. createFiles(files, to, options, function() {
  258. var out = [], err;
  259. Object.keys(options.toHash).forEach(function(k) {
  260. out.push(options.toHash[k]);
  261. });
  262. if (options.confirm) {
  263. confirm(out, options, callback);
  264. } else if (!options.errors.length) {
  265. callback(null, out.sort());
  266. } else {
  267. /*istanbul ignore next*/
  268. err = new Error('Unable to copy directory' + (out.length ? ' entirely' : ''));
  269. /*istanbul ignore next*/
  270. err.list = options.errors;
  271. /*istanbul ignore next*/
  272. callback(err, out.sort());
  273. }
  274. });
  275. });
  276. });
  277. });
  278. });
  279. };
  280. fs.stat(options.from, function(err, stat) {
  281. if (err) {
  282. return callback(new Error('From should be a file or directory'));
  283. }
  284. if (stat && stat.isDirectory()) {
  285. if (options.deleteFirst) {
  286. rimraf(to, function() {
  287. proc();
  288. });
  289. } else {
  290. proc();
  291. }
  292. } else {
  293. if (stat.isFile()) {
  294. var dirRegex = new RegExp(path.sep + '$');
  295. if (dirRegex.test(to)) { // Create directory if has trailing separator
  296. to = path.join(to, path.basename(options.from));
  297. }
  298. // ensure copyFile() can access cached stat
  299. options.stats = options.stats || {};
  300. options.stats[from] = stat;
  301. return copyFile(options.from, to, options, callback);
  302. }
  303. callback(new Error('From should be a file or directory'));
  304. }
  305. });
  306. };
  307. //Preserve backward compatibility
  308. cpr.cpr = cpr;
  309. //Export a function
  310. module.exports = cpr;