Files: ce27463164a9c7dab3164e59e793ace39b8ac23d / copy.js
7862 bytesRaw
1 | const fs = require('graceful-fs'); |
2 | const path = require('path'); |
3 | const utils = require('./utils'); |
4 | const { mkdirs, utimes } = require('fs-extra'); |
5 | |
6 | const notExist = Symbol('notExist'); |
7 | const existsReg = Symbol('existsReg'); |
8 | |
9 | module.exports = function copy(src, dest, cryptDecryptStream) { |
10 | return new Promise(async (resolve, reject) => { |
11 | src = path.resolve(src); |
12 | dest = path.resolve(dest); |
13 | |
14 | // Do not allow src and dest to be the same |
15 | if (src === dest) { |
16 | return reject(new Error('Source and destination must not be the same.')); |
17 | } |
18 | |
19 | try { |
20 | await checkParentDir(src, dest, cryptDecryptStream); |
21 | |
22 | return resolve(); |
23 | } catch (err) { |
24 | return reject(err); |
25 | } |
26 | }); |
27 | }; |
28 | |
29 | function checkParentDir (src, dest, cryptDecryptStream) { |
30 | return new Promise(async (resolve, reject) => { |
31 | const destParent = path.dirname(dest); |
32 | |
33 | try { |
34 | |
35 | // Verify if dir exists |
36 | const dirExists = await utils.exists(destParent); |
37 | |
38 | // Create dir |
39 | if (!dirExists) { |
40 | await mkdirs(destParent); |
41 | } |
42 | |
43 | // Copy items |
44 | await startCopy(src, dest, cryptDecryptStream); |
45 | |
46 | return resolve(); |
47 | } catch (err) { |
48 | return reject(err); |
49 | } |
50 | }); |
51 | } |
52 | |
53 | function startCopy (src, dest, cryptDecryptStream) { |
54 | return new Promise(async (resolve, reject) => { |
55 | try { |
56 | await getStats(src, dest, cryptDecryptStream); |
57 | |
58 | return resolve(); |
59 | } catch (err) { |
60 | return reject(err); |
61 | } |
62 | }); |
63 | } |
64 | |
65 | function getStats (src, dest, cryptDecryptStream) { |
66 | return new Promise((resolve, reject) => { |
67 | return fs.lstat(src, async (err, st) => { |
68 | try { |
69 | if (err) { |
70 | return reject(err); |
71 | } |
72 | |
73 | if (st.isDirectory()) { |
74 | await onDir(st, src, dest, cryptDecryptStream); |
75 | } else if (st.isFile() || |
76 | st.isCharacterDevice() || |
77 | st.isBlockDevice()) { |
78 | await onFile(st, src, dest, cryptDecryptStream); |
79 | } else if (st.isSymbolicLink()) { |
80 | return reject(new Error('Symbolic links are not supported.')); |
81 | } |
82 | |
83 | return resolve(); |
84 | } catch (err) { |
85 | return reject(err); |
86 | } |
87 | }); |
88 | }); |
89 | } |
90 | |
91 | function onFile (srcStat, src, dest, cryptDecryptStream) { |
92 | return new Promise(async (resolve, reject) => { |
93 | try { |
94 | const resolvedPath = await checkDest(dest); |
95 | |
96 | if (resolvedPath === notExist) { |
97 | await copyFile(srcStat, src, dest, cryptDecryptStream); |
98 | } else if (resolvedPath === existsReg) { |
99 | await mayCopyFile(srcStat, src, dest, cryptDecryptStream); |
100 | } else { |
101 | if (src === resolvedPath) { |
102 | return resolve(); |
103 | } |
104 | |
105 | await mayCopyFile(srcStat, src, dest, cryptDecryptStream); |
106 | } |
107 | } catch (err) { |
108 | return reject(err); |
109 | } |
110 | |
111 | return resolve(); |
112 | }); |
113 | } |
114 | |
115 | function mayCopyFile (srcStat, src, dest, cryptDecryptStream) { |
116 | return new Promise((resolve, reject) => { |
117 | return fs.unlink(dest, async err => { |
118 | if (err) { |
119 | return reject(err); |
120 | } |
121 | |
122 | try { |
123 | await copyFile(srcStat, src, dest, cryptDecryptStream); |
124 | } catch (err) { |
125 | return reject(err); |
126 | } |
127 | |
128 | return resolve(); |
129 | }); |
130 | }); |
131 | } |
132 | |
133 | function copyFile (srcStat, src, dest, cryptDecryptStream) { |
134 | return new Promise((resolve, reject) => { |
135 | const rs = fs.createReadStream(src); |
136 | |
137 | rs.on('error', err => reject(err)) |
138 | .once('open', () => { |
139 | const ws = fs.createWriteStream(dest, { mode: srcStat.mode }); |
140 | |
141 | ws.on('error', err => reject(err)) |
142 | .on('open', () => rs.pipe(cryptDecryptStream).pipe(ws)) |
143 | .once('close', async () => { |
144 | try { |
145 | await setDestModeAndTimestamps(srcStat, dest); |
146 | } catch (err) { |
147 | return resolve(); |
148 | } |
149 | |
150 | return resolve(); |
151 | }); |
152 | }); |
153 | }); |
154 | } |
155 | |
156 | function setDestModeAndTimestamps (srcStat, dest) { |
157 | return new Promise((resolve, reject) => { |
158 | return fs.chmod(dest, srcStat.mode, err => { |
159 | if (err) { |
160 | return reject(err); |
161 | } |
162 | |
163 | return utimes(dest, srcStat.atime, srcStat.mtime, () => { |
164 | if (err) { |
165 | return reject(err); |
166 | } |
167 | |
168 | return resolve(); |
169 | }); |
170 | }); |
171 | }); |
172 | } |
173 | |
174 | function onDir (srcStat, src, dest, cryptDecryptStream) { |
175 | return new Promise(async (resolve, reject) => { |
176 | try { |
177 | const resolvedPath = await checkDest(dest); |
178 | |
179 | if (resolvedPath === notExist) { |
180 | if (isSrcSubdir(src, dest)) { |
181 | return reject(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)); |
182 | } |
183 | |
184 | await mkDirAndCopy(srcStat, src, dest, cryptDecryptStream); |
185 | } else if (resolvedPath === existsReg) { |
186 | if (isSrcSubdir(src, dest)) { |
187 | return reject(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)); |
188 | } |
189 | |
190 | await mayCopyDir(src, dest, cryptDecryptStream); |
191 | } else { |
192 | if (src === resolvedPath) { |
193 | return reject(); |
194 | } |
195 | |
196 | await copyDir(src, dest, cryptDecryptStream); |
197 | } |
198 | } catch (err) { |
199 | return reject(err); |
200 | } |
201 | |
202 | return resolve(); |
203 | }); |
204 | } |
205 | |
206 | function mayCopyDir (src, dest, cryptDecryptStream) { |
207 | return new Promise((resolve, reject) => { |
208 | return fs.stat(dest, async (err, st) => { |
209 | if (err) { |
210 | return reject(err); |
211 | } |
212 | |
213 | if (!st.isDirectory()) { |
214 | return reject(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)); |
215 | } |
216 | |
217 | try { |
218 | await copyDir(src, dest, cryptDecryptStream); |
219 | } catch (err) { |
220 | return reject(err); |
221 | } |
222 | |
223 | return resolve(); |
224 | }); |
225 | }); |
226 | } |
227 | |
228 | function mkDirAndCopy (srcStat, src, dest, cryptDecryptStream) { |
229 | return new Promise((resolve, reject) => { |
230 | return fs.mkdir(dest, srcStat.mode, err => { |
231 | if (err) { |
232 | return reject(err); |
233 | } |
234 | |
235 | return fs.chmod(dest, srcStat.mode, async err => { |
236 | if (err) { |
237 | return reject(err); |
238 | } |
239 | |
240 | try { |
241 | await copyDir(src, dest, cryptDecryptStream); |
242 | } catch (err) { |
243 | return reject(err); |
244 | } |
245 | |
246 | return resolve(); |
247 | }); |
248 | }); |
249 | }); |
250 | } |
251 | |
252 | function copyDir (src, dest, cryptDecryptStream) { |
253 | return new Promise((resolve, reject) => { |
254 | return fs.readdir(src, async (err, items) => { |
255 | if (err) { |
256 | return reject(err); |
257 | } |
258 | |
259 | try { |
260 | await copyDirItems(items, src, dest, cryptDecryptStream); |
261 | } catch (err) { |
262 | return reject(err); |
263 | } |
264 | |
265 | return resolve(); |
266 | }); |
267 | }); |
268 | } |
269 | |
270 | function copyDirItems (items, src, dest, cryptDecryptStream) { |
271 | return new Promise(async (resolve, reject) => { |
272 | const item = items.pop(); |
273 | |
274 | if (!item) { |
275 | return reject(); |
276 | } |
277 | |
278 | try { |
279 | await startCopy(path.join(src, item), path.join(dest, item), cryptDecryptStream); |
280 | |
281 | return copyDirItems(items, src, dest, cryptDecryptStream); |
282 | } catch (err) { |
283 | return reject(err); |
284 | } |
285 | }); |
286 | } |
287 | |
288 | // Check if dest exists and/or is a symlink |
289 | const checkDest = dest => |
290 | new Promise((resolve, reject) => { |
291 | return fs.readlink(dest, (err, resolvedPath) => { |
292 | if (err) { |
293 | if (err.code === 'ENOENT') { |
294 | return resolve(notExist); |
295 | } |
296 | |
297 | // Dest exists and is a regular file or directory, Windows may throw UNKNOWN error |
298 | if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { |
299 | return resolve(existsReg); |
300 | } |
301 | |
302 | return reject(err); |
303 | } |
304 | |
305 | // Dest exists and is a symlink |
306 | return resolve(resolvedPath); |
307 | }); |
308 | }); |
309 | |
310 | // Return true if dest is a subdir of src, otherwise false |
311 | // Extract dest base dir and check if that is the same as src basename |
312 | const isSrcSubdir = (src, dest) => { |
313 | const srcArray = src.split(path.sep); |
314 | const destArray = dest.split(path.sep); |
315 | |
316 | return srcArray.reduce((acc, current, i) => |
317 | acc && destArray[i] === current, true); |
318 | }; |
Built with git-ssb-web