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