-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathautofile.js
290 lines (253 loc) · 8.7 KB
/
autofile.js
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
278
279
280
281
282
283
284
285
286
287
288
289
290
'use strict';
var glob = require('glob');
var async = require('async');
var path = require('path');
var fs = require('fs');
var mkdirp = require('mkdirp');
var mv = require('mv');
module.exports = function (task) {
task
.id('mv')
.name('Move')
.description('Move files and folders.')
.author('Indigo United')
.option('files', 'Which files should be moved. Accepts an object in which keys are the source files and values the destination. Source values support minimatch.')
.option('glob', 'The options to pass to glob (check https://npmjs.org/package/glob for details).', null)
.do(function (opt, ctx, next) {
opt.glob = opt.glob || {};
var sources = Object.keys(opt.files);
var error;
// Cycle through each source
// Note that series is used to avoid conflicts between each pattern
async.forEachSeries(sources, function (pattern, next) {
var dst = opt.files[pattern];
pattern = path.normalize(pattern);
// Expand the files to get an array of files and directories
expand(pattern, opt.glob, function (err, files, dirs, directMatch) {
if (err) {
return next(err);
}
if (!files.length && !dirs.length) {
error = new Error('ENOENT, no such file or directory \'' + pattern + '\'');
error.code = 'ENOENT';
return next(error);
}
// Process the matches
if (directMatch) {
processDirectMatch(files, dirs, dst, ctx, next);
} else {
processPatternMatch(pattern, files, dirs, dst, ctx, next);
}
});
}, next);
});
};
/**
* Processes a direct match.
*
* @param {Array} files The files
* @param {Array} dirs The directories
* @param {String} dst The destination
* @param {Object} ctx The context
* @param {Function} next The callback to call with the files and folders (follows node conventions)
*/
function processDirectMatch(files, dirs, dst, ctx, next) {
var src = files[0] || dirs[0];
var srcType = files[0] === src ? 'file' : 'dir';
var dstType;
var error;
var tmp;
// Ensure the dirname of the dst exists
mkdirp(path.dirname(dst), function (err) {
if (err) {
return next(err);
}
// Dst is a folder if:
// - if exists and is a folder
// - ends with /
fs.stat(dst, function (err, stat) {
if (stat) {
dstType = stat.isFile() ? 'file' : 'dir';
} else {
dstType = /[\/\\]$/.test(dst) ? 'dir' : srcType;
}
// Check if move is possible
if (srcType === 'dir' && dstType === 'file') {
error = new Error('ENODIR, not a directory: \'' + dst + '\'');
error.code = 'ENODIR';
return next(error);
}
// Folder to folder
if (srcType === 'dir' && dstType === 'dir') {
// When moving to a folder that already exists
// or ends with a /, the user is trying to move the folder
// inside it
if (stat || /[\/\\]$/.test(dst)) {
tmp = !stat ? dst : null;
dst = path.join(dst, path.basename(src));
}
if (tmp) {
mkdirp(tmp, function (err) {
if (err) {
return next(err);
}
move(src, dst, ctx, next);
});
} else {
move(src, dst, ctx, next);
}
// File to folder
} else if (srcType === 'file' && dstType === 'dir') {
// If moving file to dir, ensure that the dir is created
// and perform a file to file move afterwards
if (!stat) {
fs.mkdir(dst, function (err) {
if (err) {
return next(err);
}
dst = path.join(dst, path.basename(src));
move(src, dst, ctx, next);
});
} else {
dst = path.join(dst, path.basename(src));
move(src, dst, ctx, next);
}
// File to file is simple
} else {
move(src, dst, ctx, next);
}
});
});
}
/**
* Processes a pattern match.
*
* @param {String} pattern The pattern
* @param {Array} files The files
* @param {Array} dirs The directories
* @param {String} dst The destination
* @param {Object} ctx The context
* @param {Function} next The callback to call with the files and folders (follows node conventions)
*/
function processPatternMatch(pattern, files, dirs, dst, ctx, next) {
files.push.apply(files, dirs);
async.forEachLimit(files, 30, function (file, next) {
var currDst = path.join(dst, relativePath(file, pattern));
mkdirp(path.dirname(currDst), function (err) {
if (err) {
return next(err);
}
move(file, currDst, ctx, next);
});
}, next);
}
/**
* Moves a file or directory.
*
* @param {String} src The source
* @param {String} dst The destination
* @param {Object} ctx The context
* @param {Object} ctx The context
* @param {Function} next The function to call when done (follows node conventions)
*/
function move(src, dst, ctx, next) {
ctx.log.debugln('Moving ' + src + ' to ' + dst);
mv(src, dst, next);
}
/**
* Expands the given minimatch pattern to an array of files and an array of dirs.
* The dirs are guaranteed to not overlap files.
*
* @param {String} pattern The pattern
* @param {Object} options The options to pass to the glob
* @param {Function} next The callback to call with the files and folders (follows node conventions)
*/
function expand(pattern, options, next) {
var files = [];
var dirs = [];
var lastMatch = '';
options = options || {};
// If the user specified a /**/* pattern, optimize it
if (!options.glob || !options.glob.noglobstar) {
pattern = pattern.replace(/(\/\*\*\/\*)+$/g, '/*');
}
// Expand with glob
options.mark = true;
glob(pattern, options, function (err, matches) {
if (err) {
return next(err);
}
matches.forEach(function (match) {
var isFile = !/[\/\\]$/.test(match);
if (isFile) {
lastMatch = match;
files.push(lastMatch);
} else {
lastMatch = match.replace(/[\/\\]+$/, '');
dirs.push(lastMatch);
}
});
// If we only got one match and it was the same as the original pattern,
// then it was a direct match
pattern = path.normalize(pattern).replace(/[\/\\]+$/, '');
lastMatch = path.normalize(lastMatch).replace(/[\/\\]+$/, '');
var directMatch = matches.length === 1 && lastMatch === pattern;
if (!directMatch) {
cleanup(files, dirs);
}
next(null, files, dirs, directMatch);
});
}
/**
* Takes an array of files and folders and takes care of overlapping.
* See the expand function for more info.
*
* @param {Array} files The array of files
* @param {Array} dirs The array of dirs
*/
function cleanup(files, dirs) {
var x, y;
// Cleanup files that overlap dirs
dirs.forEach(function (dir) {
for (x = files.length - 1; x >= 0; --x) {
if (path.dirname(files[x]).indexOf(dir) === 0) {
files.splice(x, 1);
}
}
});
// Cleanup dirs that overlap eachother
for (x = 0; x < dirs.length; ++x) {
for (y = x + 1; y < dirs.length; ++y) {
if (dirs[y].indexOf(dirs[x] + path.sep) === 0) {
dirs.splice(y, 1);
--x;
--y;
}
}
}
}
/**
* Gets the relative path of a file relative to the pattern.
* For instance:
* file = /a/b.js
* pattern = /a/*
*
* Should return b.js
*
* @param {String} file The file
* @param {String} pattern The pattern
*
* @return {String} The relative path
*/
function relativePath(file, pattern) {
var length = file.length,
x;
pattern = path.normalize(pattern);
file = path.normalize(file);
for (x = 0; x < length; ++x) {
if (file[x] !== pattern[x]) {
return file.substr(x);
}
}
return path.basename(file);
}