diff --git a/vlib/io/util/util.v b/vlib/io/util/util.v new file mode 100644 index 0000000000..8d4031119d --- /dev/null +++ b/vlib/io/util/util.v @@ -0,0 +1,111 @@ +module util + +import os +import rand +import rand.wyrand +import rand.util as rutil + +const ( + retries = 10000 +) + +pub struct TempFileOptions { + path string = os.temp_dir() + pattern string = '' +} + +// temp_file returns an uniquely named, open, writable, `os.File` and it's path +pub fn temp_file(tfo TempFileOptions) ?(os.File, string) { + mut d := tfo.path + if d == '' { + d = os.temp_dir() + } + os.is_writable_folder(d) or { + return error(@FN + + ' could not create temporary file in "$d". Please ensure write permissions.') + } + d = d.trim_right(os.path_separator) + mut rng := rand.new_default(rand.PRNGConfigStruct{}) + prefix, suffix := prefix_and_suffix(tfo.pattern) or { + return error(@FN + ' ' + err) + } + for retry := 0; retry < retries; retry++ { + path := os.join_path(d, prefix + random_number(mut rng) + suffix) + mut mode := 'rw+' + $if windows { + mode = 'w+' + } + mut file := os.open_file(path, mode, 0o600) or { + rng.seed(rutil.time_seed_array(2)) + continue + } + if os.exists(path) && os.is_file(path) { + return file, path + } + } + return error(@FN + + ' could not create temporary file in "$d". Retry limit ($retries) exhausted. Please ensure write permissions.') +} + +pub struct TempDirOptions { + path string = os.temp_dir() + pattern string = '' +} + +// temp_dir returns an uniquely named, writable, directory path +pub fn temp_dir(tdo TempFileOptions) ?string { + mut d := tdo.path + if d == '' { + d = os.temp_dir() + } + os.is_writable_folder(d) or { + return error(@FN + + ' could not create temporary directory "$d". Please ensure write permissions.') + } + d = d.trim_right(os.path_separator) + mut rng := rand.new_default(rand.PRNGConfigStruct{}) + prefix, suffix := prefix_and_suffix(tdo.pattern) or { + return error(@FN + ' ' + err) + } + for retry := 0; retry < retries; retry++ { + path := os.join_path(d, prefix + random_number(mut rng) + suffix) + os.mkdir_all(path) or { + rng.seed(rutil.time_seed_array(2)) + continue + } + if os.is_dir(path) && os.exists(path) { + os.is_writable_folder(path) or { + return error(@FN + + ' could not create temporary directory "$d". Please ensure write permissions.') + } + return path + } + } + return error(@FN + + ' could not create temporary directory "$d". Retry limit ($retries) exhausted. Please ensure write permissions.') +} + +// * Utility functions +fn random_number(mut rng wyrand.WyRandRNG) string { + s := (u32(1e9) + (u32(os.getpid()) + rng.u32() % u32(1e9))).str() + return s.substr(1, s.len) +} + +fn prefix_and_suffix(pattern string) ?(string, string) { + mut pat := pattern + if pat.contains(os.path_separator) { + return error('pattern cannot contain path separators ($os.path_separator).') + } + pos := pat.last_index('*') or { + -1 + } + mut prefix := '' + mut suffix := '' + if pos != -1 { + prefix = pat.substr(0, pos) + suffix = pat.substr(pos + 1, pat.len) + } else { + prefix = pat + } + return prefix, suffix +} diff --git a/vlib/io/util/util_test.v b/vlib/io/util/util_test.v new file mode 100644 index 0000000000..ddc08db963 --- /dev/null +++ b/vlib/io/util/util_test.v @@ -0,0 +1,127 @@ +import os +import io.util + +const ( + // tfolder will contain all the temporary files/subfolders made by + // the different tests. It would be removed in testsuite_end(), so + // individual os tests do not need to clean up after themselves. + tfolder = os.join_path(os.temp_dir(), 'v', 'tests', 'io_util_test') +) + +fn testsuite_begin() { + eprintln('testsuite_begin, tfolder = $tfolder') + os.rmdir_all(tfolder) + assert !os.is_dir(tfolder) + os.mkdir_all(tfolder) + os.chdir(tfolder) + assert os.is_dir(tfolder) +} + +fn testsuite_end() { + os.chdir(os.wd_at_startup) + os.rmdir_all(tfolder) + assert !os.is_dir(tfolder) + // eprintln('testsuite_end , tfolder = $tfolder removed.') +} + +fn test_temp_file() { + // Test defaults + mut f, mut path := util.temp_file({}) or { + assert false + return + } + mut prev_path := path + defer { + f.close() + } + assert os.is_file(path) + assert f.is_opened + // Test pattern + f.close() + f, path = util.temp_file({ + pattern: 'some_*_test.file' + }) or { + assert false + return + } + assert path != prev_path + assert os.is_file(path) + assert f.is_opened + mut filename := os.file_name(path) + assert filename.contains('_test.file') + // Check for 9 digits where the wildcard is placed in the pattern + for i, c in filename { + if i > 4 && i <= 4 + 9 { + assert c.is_digit() + } + } + // Test custom path + prev_path = path + f.close() + f, path = util.temp_file({ + path: tfolder + }) or { + assert false + return + } + assert path != prev_path + assert os.is_file(path) + assert path.contains(tfolder) + assert f.is_opened + filename = os.file_name(path) + for c in filename { + assert c.is_digit() + } +} + +fn test_temp_dir() { + // Test defaults + mut path := util.temp_dir({}) or { + assert false + return + } + assert os.is_dir(path) + mut writable := os.is_writable_folder(path) or { + assert false + return + } + assert writable + mut prev_path := path + // Test pattern + path = util.temp_dir({ + pattern: 'some_*_test_dir' + }) or { + assert false + return + } + assert path != prev_path + assert os.is_dir(path) + mut filename := os.file_name(path) + assert filename.contains('_test_dir') + // Check for 9 digits where the wildcard is placed in the pattern + for i, c in filename { + if i > 4 && i <= 4 + 9 { + assert c.is_digit() + } + } + // Test custom path + prev_path = path + path = util.temp_dir({ + path: tfolder + }) or { + assert false + return + } + assert path != prev_path + assert os.is_dir(path) + writable = os.is_writable_folder(path) or { + assert false + return + } + assert writable + assert path.contains(tfolder) + filename = os.file_name(path) + for c in filename { + assert c.is_digit() + } +}