Files
herolib/lib/core/pathlib/path_tools.v
2025-11-25 06:01:26 +01:00

615 lines
17 KiB
V

module pathlib
import os
import incubaid.herolib.core.texttools
import incubaid.herolib.core.texttools.regext
import time
import crypto.md5
import rand
import incubaid.herolib.ui.console
// check path exists
pub fn (mut path Path) exists() bool {
// if path.cat == .unknown || path.exist == .unknown {
// path.check()
// }
path.check()
return path.exist == .yes
}
// case insentive check on paths
pub fn path_equal(a_ string, b_ string) bool {
a := os.abs_path(a_.replace('~', os.home_dir())).to_lower()
b := os.abs_path(b_.replace('~', os.home_dir())).to_lower()
return a == b
}
// rename the file or directory
pub fn (mut path Path) rename(name string) ! {
if name.contains('/') {
return error("should only be a name no dir inside: '${name}'")
}
mut dest := ''
if path.path.contains('/') {
before := path.path.all_before_last('/')
dest = before + '/' + name
} else {
dest = name
}
os.mv(path.path, dest)!
path.path = dest
path.check()
}
// TODO: make part of pathlib of Path
// uncompress to specified directory .
// if copy then will keep the original
pub fn (mut path Path) expand(dest string) !Path {
$if debug {
console.print_header('expand ${path.path}')
}
if dest.len < 4 {
return error("Path dest needs to be mentioned and +4 char. Now '${dest}'")
}
filext := os.file_ext(path.name()).to_lower()
// the ones who return a filepath
if filext == '.xz' {
cmd := 'xz --decompress ${path.path} --stdout > ${dest}'
if os.is_file(dest) {
os.rm(dest)!
}
os.mkdir_all(dest)!
os.rmdir(dest)!
res := os.execute(cmd)
// console.print_debug(res)
if res.exit_code > 0 {
// console.print_debug(cmd)
return error('Could not expand xz.\n${res}')
}
return get_file(path: dest, create: false)!
}
mut desto := get_dir(path: dest, create: true)!
desto.empty()!
if path.name().to_lower().ends_with('.tar.gz') || path.name().to_lower().ends_with('.tgz') {
cmd := 'tar -xzvf ${path.path} -C ${desto.path}'
// console.print_debug(cmd)
res := os.execute(cmd)
if res.exit_code > 0 {
return error('Could not expand.\n${res}')
}
} else if path.name().to_lower().ends_with('.zip') {
cmd := 'unzip ${path.path} -d ${dest}'
// console.print_debug(cmd)
res := os.execute(cmd)
// console.print_debug(res)
if res.exit_code > 0 {
return error('Could not expand zip.\n${res}')
}
} else if path.name().to_lower().ends_with('.bz2') {
cmd := '
bunzip2 -f -k ${path.path}
' // console.print_debug(cmd)
res := os.execute(cmd)
if res.exit_code > 0 {
return error('Could not expand bz2.\n${res.output}')
}
dest_tmp := path.path.all_before_last('.bz2')
desto.delete()!
mut desto2 := get_file(path: dest, create: false)!
os.mv(dest_tmp, desto2.path)!
return desto2
} else {
panic('expand not implemented yet for : ${path.path}')
}
return desto
}
// chown changes the owner and group attributes of path to owner and group.
pub fn (mut path Path) chown(owner int, group int) ! {
os.chown(path.path, owner, group)!
}
// chmod change file access attributes of path to mode.
// Octals like 0o600 can be used.
pub fn (mut path Path) chmod(mode int) ! {
os.chmod(path.path, mode)!
}
// get relative path in relation to destpath .
// will not resolve symlinks
pub fn (path Path) path_relative(destpath string) !string {
// console.print_header(' path relative: '$path.path' '$destpath'")
return path_relative(destpath, path.path)
}
// recursively finds the least common ancestor of array of paths .
// will always return the absolute path (relative gets changed to absolute).
pub fn find_common_ancestor(paths_ []string) string {
for p in paths_ {
if p.trim_space() == '' {
panic('cannot find commone ancestors if any of items in paths is empty.\n${paths_}')
}
}
paths := paths_.map(os.abs_path(os.real_path(it))) // get the real path (symlinks... resolved)
// console.print_debug(paths.str())
parts := paths[0].split('/')
mut totest_prev := '/'
for i in 1 .. parts.len {
totest := parts[0..i + 1].join('/')
if paths.any(!it.starts_with(totest)) {
return totest_prev
}
totest_prev = totest
}
return totest_prev
}
// same as above but will treat symlinks as if normal links
// allowing finding relative paths between links as well
// QUESTION: should we merge with above?
pub fn find_simple_common_ancestor(paths_ []string) string {
for p in paths_ {
if p.trim_space() == '' {
panic('cannot find commone ancestors if any of items in paths is empty.\n${paths_}')
}
}
paths := paths_.map(os.abs_path(it))
parts := paths[0].split('/')
mut totest_prev := '/'
for i in 1 .. parts.len {
totest := parts[0..i + 1].join('/')
if paths.any(!it.starts_with(totest)) {
return totest_prev
}
totest_prev = totest
}
return totest_prev
}
// find parent of path
pub fn (path Path) parent() !Path {
mut p := path.absolute()
parent := os.dir(p) // get parent directory
if parent == '.' || parent == '/' {
return error('no parent for path ${path.path}')
} else if parent == '' {
return Path{
path: '/'
cat: Category.dir
exist: .unknown
}
}
return Path{
path: parent
cat: Category.dir
exist: .unknown
}
}
pub struct MoveArgs {
pub mut:
dest string // path
delete bool // if true will remove files which are on dest which are not on source
chmod_execute bool
}
// move to other location
// ```
// dest string // path
// delete bool // if true will remove files which are on dest which are not on source
// ```
pub fn (mut path Path) move(args MoveArgs) ! {
mut d := get(args.dest)
if d.exists() {
if args.delete {
d.delete()!
} else {
return error("Found dest dir in move and can't delete. \n${args}")
}
}
os.mv(path.path, d.path)!
if args.chmod_execute {
d.chmod(0o770)!
}
}
// the path will move itself up 1 level .
// e.g. path is /tmp/rclone and there is /tmp/rclone/rclone-v1.64.2-linux-amd64 .
// that last dir needs to move 1 up
pub fn (mut path Path) moveup_single_subdir() ! {
mut plist := path.list(recursive: false, ignore_default: true, dirs_only: true)!
// console.print_debug(plist.str())
if plist.paths.len != 1 {
return error('could not find one subdir in ${path.path} , so cannot move up')
}
mut pdest := plist.paths[0]
pdest.moveup()!
}
// the path will move itself up 1 level .
// the e.g. /tmp/rclone/rclone-v1.64.2-linux-amd64/ -> /tmp/rclone
pub fn (mut path Path) moveup() ! {
console.print_stdout('move up: ${path}')
pdest := path.parent()!
tmpdir := '${os.temp_dir()}/${rand.u16()}'
path.move(dest: tmpdir, delete: true)!
mut tmpdirpath := get_dir(path: tmpdir)!
tmpdirpath.move(dest: pdest.path, delete: true)!
path.path = pdest.path
path.check()
}
// returns extension without .
pub fn (path Path) extension() string {
return os.file_ext(path.path).trim('.')
}
// returns extension without and all lower case
pub fn (path Path) extension_lower() string {
return path.extension().to_lower()
}
// will rewrite the path to lower_case if not the case yet
// will also remove weird chars
// if changed will return true
// the file will be moved to the new location
pub fn (mut path Path) path_normalize() !bool {
path_original := path.path + '' // make sure is copy, needed?
// if path.cat == .file || path.cat == .dir || !path.exists() {
// return error('path $path does not exist, cannot namefix (only support file and dir)')
// }
if path.extension().to_lower() == 'jpeg' {
path.path = path.path_no_ext() + '.jpg'
}
namenew := texttools.name_fix_keepext(path.name())
if namenew != path.name() {
path.path = os.join_path(os.dir(path.path), namenew)
}
if path.path != path_original {
os.mv(path_original, path.path)!
path.check()
return true
}
return false
}
// walk upwards starting from path untill dir or file tofind is found
// works recursive
pub fn (path Path) parent_find(tofind string) !Path {
if os.exists(os.join_path(path.path, tofind)) {
return path
}
path2 := path.parent()!
return path2.parent_find(tofind)
}
// parent_find_advanced walks up the directory tree, collecting all items that match tofind
// pattern until it encounters an item matching the stop pattern.
// Both tofind and stop use matcher filter format supporting wildcards:
// - '*.txt' matches any .txt file
// - 'src*' matches anything starting with 'src'
// - '.git' matches exactly '.git'
// - '*test*' matches anything containing 'test'
//
// Returns all found paths before hitting the stop condition.
// If stop is never found, continues until reaching filesystem root.
//
// Examples:
// // Find all 'test_*.v' files until reaching '.git' directory
// tests := my_path.parent_find_advanced('test_*.v', '.git')!
//
// // Find any 'Makefile*' until hitting 'node_modules'
// makefiles := my_path.parent_find_advanced('Makefile*', 'node_modules')!
//
// // Find '*.md' files until reaching '.git'
// docs := my_path.parent_find_advanced('*.md', '.git')!
pub fn (path Path) parent_find_advanced(tofind string, stop string) ![]Path {
// Start from current path or its parent if it's a file
mut search_path := path
if search_path.is_file() {
search_path = search_path.parent()!
}
// Create matchers from filter patterns
tofind_matcher := regext.new(filter: [tofind])!
stop_matcher := regext.new(filter: [stop])!
mut found_paths := []Path{}
mut current := search_path
for {
// List contents of current directory
mut items := os.ls(current.path) or { []string{} }
// Check each item in the directory
for item in items {
// Check if this is the stop pattern - if yes, halt and return
if stop_matcher.match(item) {
return found_paths
}
// Check if this matches what we're looking for
if tofind_matcher.match(item) {
full_path := os.join_path(current.path, item)
mut found_path := get(full_path)
if found_path.exists() {
found_paths << found_path
}
}
}
// Try to move to parent directory
current = current.parent() or {
// Reached filesystem root, return what we found
return found_paths
}
}
return found_paths
}
// delete
pub fn (mut path Path) rm() ! {
return path.delete()
}
// delete
pub fn (mut path Path) delete() ! {
if path.exists() {
// console.print_debug("exists: $path")
match path.cat {
.file, .linkfile, .linkdir {
os.rm(path.path.replace('//', '/'))!
}
.dir {
os.rmdir_all(path.path)!
}
.unknown {
return error('Path cannot be unknown type')
}
}
path.exist = .no
}
if os.is_link(path.path) {
os.rm(path.path.replace('//', '/'))!
}
}
// remove all content but if dir let the dir exist
pub fn (mut path Path) empty() ! {
if path.cat == .dir {
os.mkdir_all(path.path)!
path.exist = .yes
mut list := path.list()!
for mut subpath in list.paths {
subpath.delete()!
}
} else if path.cat == Category.linkfile {
mut p2 := path.getlink()!
p2.empty()!
} else {
path.write('')!
}
}
// write content to the file, check is file
// if the path is a link to a file then will change the content of the file represented by the link
pub fn (mut path Path) write(content string) ! {
if !os.exists(path.path_dir()) {
os.mkdir_all(path.path_dir())!
}
if path.exists() && path.cat == Category.linkfile {
mut pathlinked := path.getlink()!
pathlinked.write(content)!
}
if path.exists() && path.cat != Category.file && path.cat != Category.linkfile {
return error('Path must be a file for ${path}')
}
os.write_file(path.path, content)!
}
// write bytes to file
pub fn (mut path Path) writeb(content []u8) ! {
if !os.exists(path.path_dir()) {
os.mkdir_all(path.path_dir())!
}
if path.exists() && path.cat == Category.linkfile {
mut pathlinked := path.getlink()!
pathlinked.writeb(content)!
}
if path.exists() && path.cat != Category.file && path.cat != Category.linkfile {
return error('Path must be a file for ${path}')
}
os.write_file_array(path.path, content)!
}
// read content from file
pub fn (mut path Path) read() !string {
path.check()
match path.cat {
.file, .linkfile {
p := path.absolute()
if !os.exists(p) {
return error('File is not exist, ${p} is a wrong path')
}
return os.read_file(p)
}
else {
return error('Path is not a file when reading. ${path.path}')
}
}
}
// read bytes from file
pub fn (mut path Path) readb() ![]u8 {
path.check()
match path.cat {
.file, .linkfile {
p := path.absolute()
if !os.exists(p) {
return error('File does not exist, ${p} is a wrong path')
}
return os.read_bytes(p)
}
else {
return error('Path is not a file when reading. ${path.path}')
}
}
}
// recalc path between target & source .
// we only support if source_ is an existing dir, links will not be supported .
// a0 := pathlib.path_relative('$testpath/a/b/c', '$testpath/a/d.txt') or { panic(err) } .
// assert a0 == '../../d.txt' .
// a2 := pathlib.path_relative('$testpath/a/b/c', '$testpath/d.txt') or { panic(err) } .
// assert a2 == '../../../d.txt' .
// a8 := pathlib.path_relative('$testpath/a/b/c', '$testpath/a/b/c/d/e/e.txt') or { panic(err) } .
// assert a8 == 'd/e/e.txt' .
// symlinks will not be resolved, as it leads to unexpected behaviour
pub fn path_relative(source_ string, linkpath_ string) !string {
mut source := os.abs_path(source_)
mut linkpath := os.abs_path(linkpath_)
// now both start with /
mut p := get(source_)
// converts file source to dir source
if source.all_after_last('/').contains('.') {
source = source.all_before_last('/')
p = p.parent() or { return error("Parent of source ${source_} doesn't exist") }
}
p.check()
if p.cat != .dir && p.cat != .linkdir {
return error('Cannot do path_relative()! if source is not a dir Now:${source_} is ${p.cat}')
} else if !p.exists() {
return error('Cannot do path_relative()! if source doesnt exist. Now:${source_}')
}
common := find_simple_common_ancestor([source, linkpath])
// if source is common, returns source
if source.len <= common.len + 1 {
// TODO: this should be safer
path := linkpath_.trim_string_left(source)
if path.starts_with('/') {
return path[1..]
} else {
return path
}
}
mut source_short := source[(common.len)..]
mut linkpath_short := linkpath[(common.len)..]
source_short = source_short.trim_string_left('/')
linkpath_short = linkpath_short.trim_string_left('/')
// console.print_stdout('source: ${source_short}')
// console.print_stdout('link: ${linkpath_short}')
source_count := source_short.count('/')
// link_count := linkpath_short.count('/')
// console.print_debug(" + source_short:$source_short ($source_count)")
// console.print_debug(" + linkpath_short:$linkpath_short ($link_count)")
mut dest := ''
if source_short == '' { // source folder is common ancestor
dest = linkpath_short
} else {
go_up := ['../'].repeat(source_count + 1).join('')
dest = '${go_up}${linkpath_short}'
}
dest = dest.replace('//', '/')
return dest
}
@[params]
pub struct TMPWriteArgs {
pub mut:
name string // optional name to remember it more easily
tmpdir string
text string // text to put in file
path string // to overrule the path where script will be stored
ext string = 'sh'
}
// write temp file and return path
pub fn temp_write(args_ TMPWriteArgs) !string {
mut args := args_
if args.path.len == 0 {
if args.tmpdir.len == 0 {
if 'TMPDIR' in os.environ() {
args.tmpdir = os.environ()['TMPDIR'] or { '/tmp' }
} else {
args.tmpdir = '/tmp'
}
}
mut t := time.now().format_ss_milli().replace(' ', '-').replace('.', ':')
texthash := md5.hexhash(args.text)
t += '_${texthash}'
mut tmppath := '${args.tmpdir}/execscripts/${t}.${args.ext}'
if args.name.len > 0 {
tmppath = '${args.tmpdir}/execscripts/${args.name}_${t}.${args.ext}'
}
if !os.exists('${args.tmpdir}/execscripts/') {
os.mkdir('${args.tmpdir}/execscripts') or {
return error('Cannot create ${args.tmpdir}/execscripts,${err}')
}
}
if os.exists(tmppath) {
for i in 1 .. 200 {
// console.print_debug(i)
tmppath = '${args.tmpdir}/execscripts/{${t}}_${i}.${args.ext}'
if !os.exists(tmppath) {
break
}
// TODO: would be better to remove older files, e.g. if older than 1 day, remove
if i > 99 {
// os.rmdir_all('$tmpdir/execscripts')!
// return temp_write(text)
panic("should not get here, can't find temp file to write for process job.")
}
}
}
args.path = tmppath
}
os.write_file(args.path, args.text)!
os.chmod(args.path, 0o777)!
return args.path
}
// pub fn path_relative(source_ string, dest_ string) !string {
// mut source := source_.trim_right('/')
// mut dest := dest_.replace('//', '/').trim_right('/')
// // console.print_debug("path relative: '$source' '$dest' ")
// if source !="" {
// if source.starts_with('/') && !dest.starts_with('/') {
// return error('if source starts with / then dest needs to start with / as well.\n - $source\n - $dest')
// }
// if !source.starts_with('/') && dest.starts_with('/') {
// return error('if source starts with / then dest needs to start with / as well\n - $source\n - $dest')
// }
// }
// if dest.starts_with(source) {
// return dest[source.len..]
// } else {
// msg := "Destination path is not in source directory: $source_ $dest_"
// return error(msg)
// }
// }