UNPKG

9.85 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 });
22};
23
24const 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 // Default behaviour - copy everything.
35 return true;
36 };
37 }
38
39 return parsedOptions;
40};
41
42const generateNoSourceError = path => {
43 const err = new Error(`Path to copy doesn't exist ${path}`);
44 err.code = "ENOENT";
45 return err;
46};
47
48const generateDestinationExistsError = path => {
49 const err = new Error(`Destination path already exists ${path}`);
50 err.code = "EEXIST";
51 return err;
52};
53
54const inspectOptions = {
55 mode: true,
56 symlinks: "report",
57 times: true,
58 absolutePath: true
59};
60
61const shouldThrowDestinationExistsError = context => {
62 return (
63 typeof context.opts.overwrite !== "function" &&
64 context.opts.overwrite !== true
65 );
66};
67
68// ---------------------------------------------------------
69// Sync
70// ---------------------------------------------------------
71
72const 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
82const 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
90const 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
109const copySymlinkSync = (from, to) => {
110 const symlinkPointsAt = fs.readlinkSync(from);
111 try {
112 fs.symlinkSync(symlinkPointsAt, to);
113 } catch (err) {
114 // There is already file/symlink with this name on destination location.
115 // Must erase it manually, otherwise system won't allow us to place symlink there.
116 if (err.code === "EEXIST") {
117 fs.unlinkSync(to);
118 // Retry...
119 fs.symlinkSync(symlinkPointsAt, to);
120 } else {
121 throw err;
122 }
123 }
124};
125
126const 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
138const 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// Async
154// ---------------------------------------------------------
155
156const 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
173const 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
190const 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 // Force read stream to close, since write stream errored
206 // read stream serves us no purpose.
207 readStream.resume();
208
209 if (err.code === "ENOENT") {
210 // Some parent directory doesn't exits. Create it and retry.
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
245const 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 // There is already file/symlink with this name on destination location.
253 // Must erase it manually, otherwise system won't allow us to place symlink there.
254 fs.unlink(to)
255 .then(() => {
256 // Retry...
257 return fs.symlink(symlinkPointsAt, to);
258 })
259 .then(resolve, reject);
260 } else {
261 reject(err);
262 }
263 });
264 });
265 });
266};
267
268const 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 // Ha! This is none of supported file system entities. What now?
279 // Just continuing without actually copying sounds sane.
280 return Promise.resolve();
281};
282
283const 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// API
326// ---------------------------------------------------------
327
328exports.validateInput = validateInput;
329exports.sync = copySync;
330exports.async = copyAsync;