Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/core/copy.ts
3557 views
1
/*
2
* copy.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
basename,
9
dirname,
10
join,
11
relative,
12
resolve,
13
} from "../deno_ral/path.ts";
14
15
import {
16
CopyOptions,
17
ensureDirSync,
18
existsSync,
19
getFileInfoType,
20
isSubdir,
21
safeRemoveSync,
22
walkSync,
23
} from "../deno_ral/fs.ts";
24
25
import { isWindows } from "../deno_ral/platform.ts";
26
27
// emulate the Deno copySync function but read and write files manually
28
// rather than calling Deno.copyFileSync (to avoid deno's attempt to
29
// modify the file permissions, see:
30
// https://github.com/denoland/deno/blob/1c05e41f37da022971f0090b2a92e6340d230055/runtime/ops/fs.rs#L914-L916
31
// which messes with the expectations of multi-user editing scenarios in RSW / RStudio Cloud)
32
33
export function copyTo(
34
src: string,
35
dest: string,
36
options: CopyOptions = {
37
overwrite: true,
38
},
39
) {
40
src = resolve(src);
41
dest = resolve(dest);
42
43
if (src === dest) {
44
throw new Error("Source and destination cannot be the same.");
45
}
46
47
const srcStat = Deno.lstatSync(src);
48
49
if (srcStat.isDirectory && isSubdir(src, dest)) {
50
throw new Error(
51
`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`,
52
);
53
}
54
55
if (srcStat.isSymlink) {
56
copySymlinkSync(src, dest, options);
57
} else if (srcStat.isDirectory) {
58
copyDirSync(src, dest, options);
59
} else if (srcStat.isFile) {
60
copyFileSync(src, dest, options);
61
}
62
}
63
64
export function copyMinimal(
65
srcDir: string,
66
destDir: string,
67
skip?: RegExp[],
68
filter?: (path: string) => boolean,
69
) {
70
// 2022-02-16: 0.125.0 walkSync appears to throw in the presence of .DS_Store
71
skip = [...(skip || []), /\.DS_Store/];
72
73
// build list of src files
74
const srcFiles: string[] = [];
75
for (
76
const walk of walkSync(
77
srcDir,
78
{
79
includeDirs: false,
80
followSymlinks: false,
81
skip,
82
},
83
)
84
) {
85
// alias source file
86
const srcFile = walk.path;
87
88
// apply filter
89
if (filter && !filter(srcFile)) {
90
continue;
91
}
92
93
// add to src files
94
srcFiles.push(srcFile);
95
}
96
97
// copy src files
98
for (const srcFile of srcFiles) {
99
if (!existsSync(srcFile)) {
100
continue;
101
}
102
const destFile = join(destDir, relative(srcDir, srcFile));
103
copyFileIfNewer(srcFile, destFile);
104
}
105
}
106
107
export function copyFileIfNewer(srcFile: string, destFile: string) {
108
// helper to perform the copy
109
const doCopy = () => {
110
copyTo(srcFile, destFile, {
111
overwrite: true,
112
preserveTimestamps: true,
113
});
114
};
115
116
// ensure target dir
117
ensureDirSync(dirname(destFile));
118
119
// avoid copy if the file exists and we can validate that the src and dest
120
// files have the same timestamp (there can be statSync errors in the case of
121
// e.g. symlinks across volumsn so we also do the copy on errors accessing
122
// file info)
123
try {
124
if (existsSync(destFile)) {
125
const srcInfo = Deno.statSync(srcFile);
126
const destInfo = Deno.statSync(destFile);
127
if (!srcInfo.mtime || !destInfo.mtime || destInfo.mtime < srcInfo.mtime) {
128
doCopy();
129
}
130
} else {
131
doCopy();
132
}
133
} catch {
134
doCopy();
135
}
136
}
137
138
function copyFileSync(
139
src: string,
140
dest: string,
141
options: InternalCopyOptions,
142
): void {
143
ensureValidCopySync(src, dest, options);
144
145
// remove the file first so that Deno.writeFileSync doesn't end up trying to
146
// re-write the file permissions (which isn't allowed by the OS if there are
147
// multiple users/owners in play). see this code for where this occurs:
148
// https://github.com/denoland/deno/blob/1c05e41f37da022971f0090b2a92e6340d230055/runtime/ops/fs.rs#L914-L916
149
if (existsSync(dest)) {
150
safeRemoveSync(dest);
151
}
152
Deno.copyFileSync(src, dest);
153
154
if (options.preserveTimestamps) {
155
const statInfo = Deno.statSync(src);
156
if (statInfo.atime && statInfo.mtime) {
157
Deno.utimeSync(dest, statInfo.atime, statInfo.mtime);
158
}
159
}
160
}
161
162
function copyDirSync(src: string, dest: string, options: CopyOptions): void {
163
const destStat = ensureValidCopySync(src, dest, {
164
...options,
165
isFolder: true,
166
});
167
168
if (!destStat) {
169
ensureDirSync(dest);
170
}
171
172
if (options.preserveTimestamps) {
173
const srcStatInfo = Deno.statSync(src);
174
if (srcStatInfo.atime && srcStatInfo.mtime) {
175
Deno.utimeSync(dest, srcStatInfo.atime, srcStatInfo.mtime);
176
}
177
}
178
179
for (const entry of Deno.readDirSync(src)) {
180
const srcPath = join(src, entry.name);
181
const destPath = join(dest, basename(srcPath as string));
182
if (entry.isSymlink) {
183
copySymlinkSync(srcPath, destPath, options);
184
} else if (entry.isDirectory) {
185
copyDirSync(srcPath, destPath, options);
186
} else if (entry.isFile) {
187
copyFileSync(srcPath, destPath, options);
188
}
189
}
190
}
191
192
function copySymlinkSync(
193
src: string,
194
dest: string,
195
options: InternalCopyOptions,
196
): void {
197
ensureValidCopySync(src, dest, options);
198
// remove dest if it exists
199
if (existsSync(dest)) {
200
safeRemoveSync(dest);
201
}
202
const originSrcFilePath = Deno.readLinkSync(src);
203
const type = getFileInfoType(Deno.lstatSync(src));
204
if (isWindows) {
205
Deno.symlinkSync(originSrcFilePath, dest, {
206
type: type === "dir" ? "dir" : "file",
207
});
208
} else {
209
Deno.symlinkSync(originSrcFilePath, dest);
210
}
211
212
if (options.preserveTimestamps) {
213
const statInfo = Deno.lstatSync(src);
214
if (statInfo.atime && statInfo.mtime) {
215
Deno.utimeSync(dest, statInfo.atime, statInfo.mtime);
216
}
217
}
218
}
219
220
interface InternalCopyOptions extends CopyOptions {
221
/**
222
* default is `false`
223
*/
224
isFolder?: boolean;
225
}
226
227
function ensureValidCopySync(
228
src: string,
229
dest: string,
230
options: InternalCopyOptions,
231
): Deno.FileInfo | undefined {
232
let destStat: Deno.FileInfo;
233
try {
234
destStat = Deno.lstatSync(dest);
235
} catch (err) {
236
if (err instanceof Deno.errors.NotFound) {
237
return;
238
}
239
throw err;
240
}
241
242
if (options.isFolder && !destStat.isDirectory) {
243
throw new Error(
244
`Cannot overwrite non-directory '${dest}' with directory '${src}'.`,
245
);
246
}
247
if (!options.overwrite) {
248
throw new Error(`'${dest}' already exists.`);
249
}
250
251
return destStat;
252
}
253
254