UNPKG

9.99 kBJavaScriptView Raw
1"use strict";
2
3const pathUtil = require("path");
4const fs = require("./utils/fs");
5const dir = require("./dir");
6const exists = require("./exists");
7const inspect = require("./inspect");
8const write = require("./write");
9const matcher = require("./utils/matcher");
10const fileMode = require("./utils/mode");
11const treeWalker = require("./utils/tree_walker");
12const validate = require("./utils/validate");
13
14const 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
25const 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 // Default behaviour - copy everything.
44 return true;
45 };
46 }
47
48 return parsedOptions;
49};
50
51const generateNoSourceError = path => {
52 const err = new Error(`Path to copy doesn't exist ${path}`);
53 err.code = "ENOENT";
54 return err;
55};
56
57const generateDestinationExistsError = path => {
58 const err = new Error(`Destination path already exists ${path}`);
59 err.code = "EEXIST";
60 return err;
61};
62
63const inspectOptions = {
64 mode: true,
65 symlinks: "report",
66 times: true,
67 absolutePath: true
68};
69
70const shouldThrowDestinationExistsError = context => {
71 return (
72 typeof context.opts.overwrite !== "function" &&
73 context.opts.overwrite !== true
74 );
75};
76
77// ---------------------------------------------------------
78// Sync
79// ---------------------------------------------------------
80
81const 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
91const 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
99const 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
118const copySymlinkSync = (from, to) => {
119 const symlinkPointsAt = fs.readlinkSync(from);
120 try {
121 fs.symlinkSync(symlinkPointsAt, to);
122 } catch (err) {
123 // There is already file/symlink with this name on destination location.
124 // Must erase it manually, otherwise system won't allow us to place symlink there.
125 if (err.code === "EEXIST") {
126 fs.unlinkSync(to);
127 // Retry...
128 fs.symlinkSync(symlinkPointsAt, to);
129 } else {
130 throw err;
131 }
132 }
133};
134
135const 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
147const 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// Async
163// ---------------------------------------------------------
164
165const 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
182const 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
199const 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 // Force read stream to close, since write stream errored
215 // read stream serves us no purpose.
216 readStream.resume();
217
218 if (err.code === "ENOENT") {
219 // Some parent directory doesn't exits. Create it and retry.
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
254const 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 // There is already file/symlink with this name on destination location.
262 // Must erase it manually, otherwise system won't allow us to place symlink there.
263 fs.unlink(to)
264 .then(() => {
265 // Retry...
266 return fs.symlink(symlinkPointsAt, to);
267 })
268 .then(resolve, reject);
269 } else {
270 reject(err);
271 }
272 });
273 });
274 });
275};
276
277const 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 // Ha! This is none of supported file system entities. What now?
288 // Just continuing without actually copying sounds sane.
289 return Promise.resolve();
290};
291
292const 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// API
335// ---------------------------------------------------------
336
337exports.validateInput = validateInput;
338exports.sync = copySync;
339exports.async = copyAsync;