-
Notifications
You must be signed in to change notification settings - Fork 2.7k
/
fs-normalized.js
174 lines (152 loc) · 5.54 KB
/
fs-normalized.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/* @flow */
// This module serves as a wrapper for file operations that are inconsistant across node and OS versions.
import fs from 'fs';
import {promisify} from './promise.js';
import {constants} from './fs';
export type CopyFileAction = {
src: string,
dest: string,
atime: Date,
mtime: Date,
mode: number,
};
let disableTimestampCorrection: ?boolean = undefined; // OS dependent. will be detected on first file copy.
const readFileBuffer = promisify(fs.readFile);
const close: (fd: number) => Promise<void> = promisify(fs.close);
const lstat: (path: string) => Promise<fs.Stats> = promisify(fs.lstat);
const open: (path: string, flags: string | number, mode: number) => Promise<number> = promisify(fs.open);
const futimes: (fd: number, atime: number, mtime: number) => Promise<void> = promisify(fs.futimes);
const write: (
fd: number,
buffer: Buffer,
offset: ?number,
length: ?number,
position: ?number,
) => Promise<void> = promisify(fs.write);
export const unlink: (path: string) => Promise<void> = promisify(require('rimraf'));
/**
* Unlinks the destination to force a recreation. This is needed on case-insensitive file systems
* to force the correct naming when the filename has changed only in character-casing. (Jest -> jest).
*/
export const copyFile = async function(data: CopyFileAction, cleanup: () => mixed): Promise<void> {
// $FlowFixMe: Flow doesn't currently support COPYFILE_FICLONE
const ficloneFlag = constants.COPYFILE_FICLONE || 0;
try {
await unlink(data.dest);
await copyFilePoly(data.src, data.dest, ficloneFlag, data);
} finally {
if (cleanup) {
cleanup();
}
}
};
// Node 8.5.0 introduced `fs.copyFile` which is much faster, so use that when available.
// Otherwise we fall back to reading and writing files as buffers.
const copyFilePoly: (src: string, dest: string, flags: number, data: CopyFileAction) => Promise<void> = (
src,
dest,
flags,
data,
) => {
if (fs.copyFile) {
return new Promise((resolve, reject) =>
fs.copyFile(src, dest, flags, err => {
if (err) {
reject(err);
} else {
fixTimes(undefined, dest, data).then(() => resolve()).catch(ex => reject(ex));
}
}),
);
} else {
return copyWithBuffer(src, dest, flags, data);
}
};
const copyWithBuffer: (src: string, dest: string, flags: number, data: CopyFileAction) => Promise<void> = async (
src,
dest,
flags,
data,
) => {
// Use open -> write -> futimes -> close sequence to avoid opening the file twice:
// one with writeFile and one with utimes
const fd = await open(dest, 'w', data.mode);
try {
const buffer = await readFileBuffer(src);
await write(fd, buffer, 0, buffer.length);
await fixTimes(fd, dest, data);
} finally {
await close(fd);
}
};
// We want to preserve file timestamps when copying a file, since yarn uses them to decide if a file has
// changed compared to the cache.
// There are some OS specific cases here:
// * On linux, fs.copyFile does not preserve timestamps, but does on OSX and Win.
// * On windows, you must open a file with write permissions to call `fs.futimes`.
// * On OSX you can open with read permissions and still call `fs.futimes`.
async function fixTimes(fd: ?number, dest: string, data: CopyFileAction): Promise<void> {
const doOpen = fd === undefined;
let openfd: number = fd ? fd : -1;
if (disableTimestampCorrection === undefined) {
// if timestamps match already, no correction is needed.
// the need to correct timestamps varies based on OS and node versions.
const destStat = await lstat(dest);
disableTimestampCorrection = fileDatesEqual(destStat.mtime, data.mtime);
}
if (disableTimestampCorrection) {
return;
}
if (doOpen) {
try {
openfd = await open(dest, 'a', data.mode);
} catch (er) {
// file is likely read-only
try {
openfd = await open(dest, 'r', data.mode);
} catch (err) {
// We can't even open this file for reading.
return;
}
}
}
try {
if (openfd) {
await futimes(openfd, data.atime, data.mtime);
}
} catch (er) {
// If `futimes` throws an exception, we probably have a case of a read-only file on Windows.
// In this case we can just return. The incorrect timestamp will just cause that file to be recopied
// on subsequent installs, which will effect yarn performance but not break anything.
} finally {
if (doOpen && openfd) {
await close(openfd);
}
}
}
// Compare file timestamps.
// Some versions of Node on windows zero the milliseconds when utime is used.
export const fileDatesEqual = (a: Date, b: Date) => {
const aTime = a.getTime();
const bTime = b.getTime();
if (process.platform !== 'win32') {
return aTime === bTime;
}
// See https://github.com/nodejs/node/pull/12607
// Submillisecond times from stat and utimes are truncated on Windows,
// causing a file with mtime 8.0079998 and 8.0081144 to become 8.007 and 8.008
// and making it impossible to update these files to their correct timestamps.
if (Math.abs(aTime - bTime) <= 1) {
return true;
}
const aTimeSec = Math.floor(aTime / 1000);
const bTimeSec = Math.floor(bTime / 1000);
// See https://github.com/nodejs/node/issues/2069
// Some versions of Node on windows zero the milliseconds when utime is used
// So if any of the time has a milliseconds part of zero we suspect that the
// bug is present and compare only seconds.
if (aTime - aTimeSec * 1000 === 0 || bTime - bTimeSec * 1000 === 0) {
return aTimeSec === bTimeSec;
}
return aTime === bTime;
};