Skip to content

Commit ecd0018

Browse files
os: enhance os.cp() to mimic Python's shutil.copy2() (#25893)
1 parent 48dc721 commit ecd0018

File tree

3 files changed

+75
-19
lines changed

3 files changed

+75
-19
lines changed

vlib/os/os.c.v

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -231,29 +231,48 @@ pub fn rename(src string, dst string) ! {
231231
}
232232
}
233233

234-
// cp copies files or folders from `src` to `dst`.
235-
pub fn cp(src string, dst string) ! {
234+
@[params]
235+
pub struct CopyParams {
236+
pub:
237+
fail_if_exists bool
238+
}
239+
240+
// cp copies the file src to the file or directory dst. If dst specifies a directory, the file will be copied into dst
241+
// using the base filename from src. If dst specifies a file that already exists, it will be replaced by
242+
// default. Can be overridden to fail by setting fail_if_exists: true
243+
pub fn cp(src string, dst string, config CopyParams) ! {
236244
$if windows {
237245
w_src := src.replace('/', '\\')
238-
w_dst := dst.replace('/', '\\')
239-
if C.CopyFile(w_src.to_wide(), w_dst.to_wide(), false) == 0 {
246+
mut w_dst := dst.replace('/', '\\')
247+
if is_dir(w_dst) {
248+
w_dst = join_path_single(w_dst, file_name(w_src))
249+
}
250+
if C.CopyFile(w_src.to_wide(), w_dst.to_wide(), config.fail_if_exists) == 0 {
240251
// we must save error immediately, or it will be overwritten by other API function calls.
241252
code := int(C.GetLastError())
242253
return error_win32(
243-
msg: 'failed to copy ${src} to ${dst}'
254+
msg: 'cp: failed to copy ${src} to ${dst}'
244255
code: code
245256
)
246257
}
247258
} $else {
259+
mut w_dst := dst
260+
if is_dir(dst) {
261+
w_dst = join_path_single(w_dst, file_name(src))
262+
}
248263
fp_from := C.open(&char(src.str), C.O_RDONLY, 0)
249264
if fp_from < 0 { // Check if file opened
250-
return error_with_code('cp: failed to open ${src}', int(fp_from))
265+
return error_with_code('cp: failed to open ${src} for reading', int(fp_from))
266+
}
267+
mode_flags := C.S_IWUSR | C.S_IRUSR
268+
mut open_flags := C.O_WRONLY | C.O_CREAT | C.O_TRUNC
269+
if config.fail_if_exists {
270+
open_flags |= C.O_EXCL
251271
}
252-
fp_to := C.open(&char(dst.str), C.O_WRONLY | C.O_CREAT | C.O_TRUNC, C.S_IWUSR | C.S_IRUSR)
272+
fp_to := C.open(&char(w_dst.str), open_flags, mode_flags)
253273
if fp_to < 0 { // Check if file opened (permissions problems ...)
254274
C.close(fp_from)
255-
return error_with_code('cp (permission): failed to write to ${dst} (fp_to: ${fp_to})',
256-
int(fp_to))
275+
return error_with_code('cp: failed to open ${w_dst} for writing', int(fp_to))
257276
}
258277
// TODO: use defer{} to close files in case of error or return.
259278
// Currently there is a C-Error when building.
@@ -267,14 +286,14 @@ pub fn cp(src string, dst string) ! {
267286
if C.write(fp_to, &buf[0], count) < 0 {
268287
C.close(fp_to)
269288
C.close(fp_from)
270-
return error_with_code('cp: failed to write to ${dst}', int(-1))
289+
return error_with_code('cp: failed to write to ${w_dst}', int(-1))
271290
}
272291
}
273292
from_attr := stat(src)!
274-
if C.chmod(&char(dst.str), from_attr.mode) < 0 {
293+
if C.chmod(&char(w_dst.str), from_attr.mode) < 0 {
275294
C.close(fp_to)
276295
C.close(fp_from)
277-
return error_with_code('failed to set permissions for ${dst}', int(-1))
296+
return error_with_code('failed to set permissions for ${w_dst}', int(-1))
278297
}
279298
C.close(fp_to)
280299
C.close(fp_from)

vlib/os/os_js.js.v

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,12 @@ pub fn rm(path string) ! {
109109
}
110110
}
111111

112-
pub fn cp(src string, dst string) ! {
112+
@[params]
113+
pub struct CopyParams {
114+
fail_if_exists bool
115+
}
116+
117+
pub fn cp(src string, dst string, config CopyParams) ! {
113118
$if js_node {
114119
err := ''
115120
#try {

vlib/os/os_test.c.v

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -328,13 +328,45 @@ fn test_walk() {
328328
fn test_cp() {
329329
old_file_name := 'cp_example.txt'
330330
new_file_name := 'cp_new_example.txt'
331-
os.write_file(old_file_name, 'Test data 1 2 3, V is awesome #$%^[]!~⭐') or { panic(err) }
332-
os.cp(old_file_name, new_file_name) or { panic(err) }
333-
old_file := os.read_file(old_file_name) or { panic(err) }
334-
new_file := os.read_file(new_file_name) or { panic(err) }
331+
os.write_file(old_file_name, 'Test data 1 2 3, V is awesome #$%^[]!~⭐')!
332+
os.cp(old_file_name, new_file_name)!
333+
old_file := os.read_file(old_file_name)!
334+
new_file := os.read_file(new_file_name)!
335335
assert old_file == new_file
336-
os.rm(old_file_name) or { panic(err) }
337-
os.rm(new_file_name) or { panic(err) }
336+
os.rm(old_file_name)!
337+
os.rm(new_file_name)!
338+
}
339+
340+
fn test_cp_to_folder() {
341+
file_name := 'cp_to_folder_example.txt'
342+
folder := 'test_cp_to_folder'
343+
os.mkdir(folder)!
344+
os.write_file(file_name, 'Test data 1 2 3, V is awesome #$%^[]!~⭐')!
345+
os.cp(file_name, folder)!
346+
new_file_path := os.join_path_single(folder, file_name)
347+
old_file := os.read_file(file_name)!
348+
new_file := os.read_file(new_file_path)!
349+
assert old_file == new_file
350+
os.rm(file_name)!
351+
os.rm(new_file_path)!
352+
os.rmdir(folder)!
353+
}
354+
355+
fn test_cp_fail_if_exists() {
356+
file_name := 'cp_fail_if_exists_example.txt'
357+
folder := 'test_cp_fail_if_exists'
358+
os.mkdir(folder)!
359+
os.write_file(file_name, 'Test data 1 2 3, V is awesome #$%^[]!~⭐')!
360+
os.cp(file_name, folder)!
361+
new_file_path := os.join_path_single(folder, file_name)
362+
if _ := os.cp(file_name, folder, fail_if_exists: true) {
363+
assert false
364+
} else {
365+
assert err.str().starts_with('cp: failed to '), 'cp err: ${err}'
366+
}
367+
os.rm(file_name)!
368+
os.rm(new_file_path)!
369+
os.rmdir(folder)!
338370
}
339371

340372
fn test_mv() {

0 commit comments

Comments
 (0)