1 | "use strict";
|
2 |
|
3 | const pathUtil = require("path");
|
4 | const fs = require("./utils/fs");
|
5 | const dir = require("./dir");
|
6 | const exists = require("./exists");
|
7 | const inspect = require("./inspect");
|
8 | const write = require("./write");
|
9 | const matcher = require("./utils/matcher");
|
10 | const fileMode = require("./utils/mode");
|
11 | const treeWalker = require("./utils/tree_walker");
|
12 | const validate = require("./utils/validate");
|
13 |
|
14 | const validateInput = (methodName, from, to, options) => {
|
15 | const methodSignature = `${methodName}(from, to, [options])`;
|
16 | validate.argument(methodSignature, "from", from, ["string"]);
|
17 | validate.argument(methodSignature, "to", to, ["string"]);
|
18 | validate.options(methodSignature, "options", options, {
|
19 | overwrite: ["boolean", "function"],
|
20 | matching: ["string", "array of string"],
|
21 | ignoreCase: ["boolean"]
|
22 | });
|
23 | };
|
24 |
|
25 | const parseOptions = (options, from) => {
|
26 | const opts = options || {};
|
27 | const parsedOptions = {};
|
28 |
|
29 | if (opts.ignoreCase === undefined) {
|
30 | opts.ignoreCase = false;
|
31 | }
|
32 |
|
33 | parsedOptions.overwrite = opts.overwrite;
|
34 |
|
35 | if (opts.matching) {
|
36 | parsedOptions.allowedToCopy = matcher.create(
|
37 | from,
|
38 | opts.matching,
|
39 | opts.ignoreCase
|
40 | );
|
41 | } else {
|
42 | parsedOptions.allowedToCopy = () => {
|
43 |
|
44 | return true;
|
45 | };
|
46 | }
|
47 |
|
48 | return parsedOptions;
|
49 | };
|
50 |
|
51 | const generateNoSourceError = path => {
|
52 | const err = new Error(`Path to copy doesn't exist ${path}`);
|
53 | err.code = "ENOENT";
|
54 | return err;
|
55 | };
|
56 |
|
57 | const generateDestinationExistsError = path => {
|
58 | const err = new Error(`Destination path already exists ${path}`);
|
59 | err.code = "EEXIST";
|
60 | return err;
|
61 | };
|
62 |
|
63 | const inspectOptions = {
|
64 | mode: true,
|
65 | symlinks: "report",
|
66 | times: true,
|
67 | absolutePath: true
|
68 | };
|
69 |
|
70 | const shouldThrowDestinationExistsError = context => {
|
71 | return (
|
72 | typeof context.opts.overwrite !== "function" &&
|
73 | context.opts.overwrite !== true
|
74 | );
|
75 | };
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | const checksBeforeCopyingSync = (from, to, opts) => {
|
82 | if (!exists.sync(from)) {
|
83 | throw generateNoSourceError(from);
|
84 | }
|
85 |
|
86 | if (exists.sync(to) && !opts.overwrite) {
|
87 | throw generateDestinationExistsError(to);
|
88 | }
|
89 | };
|
90 |
|
91 | const canOverwriteItSync = context => {
|
92 | if (typeof context.opts.overwrite === "function") {
|
93 | const destInspectData = inspect.sync(context.destPath, inspectOptions);
|
94 | return context.opts.overwrite(context.srcInspectData, destInspectData);
|
95 | }
|
96 | return context.opts.overwrite === true;
|
97 | };
|
98 |
|
99 | const copyFileSync = (srcPath, destPath, mode, context) => {
|
100 | const data = fs.readFileSync(srcPath);
|
101 | try {
|
102 | fs.writeFileSync(destPath, data, { mode, flag: "wx" });
|
103 | } catch (err) {
|
104 | if (err.code === "ENOENT") {
|
105 | write.sync(destPath, data, { mode });
|
106 | } else if (err.code === "EEXIST") {
|
107 | if (canOverwriteItSync(context)) {
|
108 | fs.writeFileSync(destPath, data, { mode });
|
109 | } else if (shouldThrowDestinationExistsError(context)) {
|
110 | throw generateDestinationExistsError(context.destPath);
|
111 | }
|
112 | } else {
|
113 | throw err;
|
114 | }
|
115 | }
|
116 | };
|
117 |
|
118 | const copySymlinkSync = (from, to) => {
|
119 | const symlinkPointsAt = fs.readlinkSync(from);
|
120 | try {
|
121 | fs.symlinkSync(symlinkPointsAt, to);
|
122 | } catch (err) {
|
123 |
|
124 |
|
125 | if (err.code === "EEXIST") {
|
126 | fs.unlinkSync(to);
|
127 |
|
128 | fs.symlinkSync(symlinkPointsAt, to);
|
129 | } else {
|
130 | throw err;
|
131 | }
|
132 | }
|
133 | };
|
134 |
|
135 | const copyItemSync = (srcPath, srcInspectData, destPath, opts) => {
|
136 | const context = { srcPath, destPath, srcInspectData, opts };
|
137 | const mode = fileMode.normalizeFileMode(srcInspectData.mode);
|
138 | if (srcInspectData.type === "dir") {
|
139 | dir.createSync(destPath, { mode });
|
140 | } else if (srcInspectData.type === "file") {
|
141 | copyFileSync(srcPath, destPath, mode, context);
|
142 | } else if (srcInspectData.type === "symlink") {
|
143 | copySymlinkSync(srcPath, destPath);
|
144 | }
|
145 | };
|
146 |
|
147 | const copySync = (from, to, options) => {
|
148 | const opts = parseOptions(options, from);
|
149 |
|
150 | checksBeforeCopyingSync(from, to, opts);
|
151 |
|
152 | treeWalker.sync(from, { inspectOptions }, (srcPath, srcInspectData) => {
|
153 | const rel = pathUtil.relative(from, srcPath);
|
154 | const destPath = pathUtil.resolve(to, rel);
|
155 | if (opts.allowedToCopy(srcPath, destPath, srcInspectData)) {
|
156 | copyItemSync(srcPath, srcInspectData, destPath, opts);
|
157 | }
|
158 | });
|
159 | };
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 | const checksBeforeCopyingAsync = (from, to, opts) => {
|
166 | return exists
|
167 | .async(from)
|
168 | .then(srcPathExists => {
|
169 | if (!srcPathExists) {
|
170 | throw generateNoSourceError(from);
|
171 | } else {
|
172 | return exists.async(to);
|
173 | }
|
174 | })
|
175 | .then(destPathExists => {
|
176 | if (destPathExists && !opts.overwrite) {
|
177 | throw generateDestinationExistsError(to);
|
178 | }
|
179 | });
|
180 | };
|
181 |
|
182 | const canOverwriteItAsync = context => {
|
183 | return new Promise((resolve, reject) => {
|
184 | if (typeof context.opts.overwrite === "function") {
|
185 | inspect
|
186 | .async(context.destPath, inspectOptions)
|
187 | .then(destInspectData => {
|
188 | resolve(
|
189 | context.opts.overwrite(context.srcInspectData, destInspectData)
|
190 | );
|
191 | })
|
192 | .catch(reject);
|
193 | } else {
|
194 | resolve(context.opts.overwrite === true);
|
195 | }
|
196 | });
|
197 | };
|
198 |
|
199 | const copyFileAsync = (srcPath, destPath, mode, context, runOptions) => {
|
200 | return new Promise((resolve, reject) => {
|
201 | const runOpts = runOptions || {};
|
202 |
|
203 | let flags = "wx";
|
204 | if (runOpts.overwrite) {
|
205 | flags = "w";
|
206 | }
|
207 |
|
208 | const readStream = fs.createReadStream(srcPath);
|
209 | const writeStream = fs.createWriteStream(destPath, { mode, flags });
|
210 |
|
211 | readStream.on("error", reject);
|
212 |
|
213 | writeStream.on("error", err => {
|
214 |
|
215 |
|
216 | readStream.resume();
|
217 |
|
218 | if (err.code === "ENOENT") {
|
219 |
|
220 | dir
|
221 | .createAsync(pathUtil.dirname(destPath))
|
222 | .then(() => {
|
223 | copyFileAsync(srcPath, destPath, mode, context).then(
|
224 | resolve,
|
225 | reject
|
226 | );
|
227 | })
|
228 | .catch(reject);
|
229 | } else if (err.code === "EEXIST") {
|
230 | canOverwriteItAsync(context)
|
231 | .then(canOverwite => {
|
232 | if (canOverwite) {
|
233 | copyFileAsync(srcPath, destPath, mode, context, {
|
234 | overwrite: true
|
235 | }).then(resolve, reject);
|
236 | } else if (shouldThrowDestinationExistsError(context)) {
|
237 | reject(generateDestinationExistsError(destPath));
|
238 | } else {
|
239 | resolve();
|
240 | }
|
241 | })
|
242 | .catch(reject);
|
243 | } else {
|
244 | reject(err);
|
245 | }
|
246 | });
|
247 |
|
248 | writeStream.on("finish", resolve);
|
249 |
|
250 | readStream.pipe(writeStream);
|
251 | });
|
252 | };
|
253 |
|
254 | const copySymlinkAsync = (from, to) => {
|
255 | return fs.readlink(from).then(symlinkPointsAt => {
|
256 | return new Promise((resolve, reject) => {
|
257 | fs.symlink(symlinkPointsAt, to)
|
258 | .then(resolve)
|
259 | .catch(err => {
|
260 | if (err.code === "EEXIST") {
|
261 |
|
262 |
|
263 | fs.unlink(to)
|
264 | .then(() => {
|
265 |
|
266 | return fs.symlink(symlinkPointsAt, to);
|
267 | })
|
268 | .then(resolve, reject);
|
269 | } else {
|
270 | reject(err);
|
271 | }
|
272 | });
|
273 | });
|
274 | });
|
275 | };
|
276 |
|
277 | const copyItemAsync = (srcPath, srcInspectData, destPath, opts) => {
|
278 | const context = { srcPath, destPath, srcInspectData, opts };
|
279 | const mode = fileMode.normalizeFileMode(srcInspectData.mode);
|
280 | if (srcInspectData.type === "dir") {
|
281 | return dir.createAsync(destPath, { mode });
|
282 | } else if (srcInspectData.type === "file") {
|
283 | return copyFileAsync(srcPath, destPath, mode, context);
|
284 | } else if (srcInspectData.type === "symlink") {
|
285 | return copySymlinkAsync(srcPath, destPath);
|
286 | }
|
287 |
|
288 |
|
289 | return Promise.resolve();
|
290 | };
|
291 |
|
292 | const copyAsync = (from, to, options) => {
|
293 | return new Promise((resolve, reject) => {
|
294 | const opts = parseOptions(options, from);
|
295 |
|
296 | checksBeforeCopyingAsync(from, to, opts)
|
297 | .then(() => {
|
298 | let allFilesDelivered = false;
|
299 | let filesInProgress = 0;
|
300 |
|
301 | const stream = treeWalker
|
302 | .stream(from, { inspectOptions })
|
303 | .on("readable", () => {
|
304 | const item = stream.read();
|
305 | if (item) {
|
306 | const rel = pathUtil.relative(from, item.path);
|
307 | const destPath = pathUtil.resolve(to, rel);
|
308 | if (opts.allowedToCopy(item.path, item.item, destPath)) {
|
309 | filesInProgress += 1;
|
310 | copyItemAsync(item.path, item.item, destPath, opts)
|
311 | .then(() => {
|
312 | filesInProgress -= 1;
|
313 | if (allFilesDelivered && filesInProgress === 0) {
|
314 | resolve();
|
315 | }
|
316 | })
|
317 | .catch(reject);
|
318 | }
|
319 | }
|
320 | })
|
321 | .on("error", reject)
|
322 | .on("end", () => {
|
323 | allFilesDelivered = true;
|
324 | if (allFilesDelivered && filesInProgress === 0) {
|
325 | resolve();
|
326 | }
|
327 | });
|
328 | })
|
329 | .catch(reject);
|
330 | });
|
331 | };
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 | exports.validateInput = validateInput;
|
338 | exports.sync = copySync;
|
339 | exports.async = copyAsync;
|