Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified thirdparty/libgc/libgc-linux-amd64.a
Binary file not shown.
81 changes: 81 additions & 0 deletions y/fs/dir.sp
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
module fs

import strings

pub struct UnableToRemoveFilesError {
files_problems []UnableToRemoveFileInfo
}

pub fn (err UnableToRemoveFilesError) msg() -> string {
mut message_builder := strings.new_builder(err.files_problems.len)
message_builder.write_string("Unable to remove files:").unwrap()

for info in err.files_problems {
message_builder.write_string("\n\t${info.path}: ${info.reason}").unwrap()
}

return message_builder.str()
}

pub fn (err UnableToRemoveFilesError) source() -> ?Error {
return none
}

pub struct UnableToRemoveFileInfo {
path string
reason string
}

// mkdir_all will create a valid full path of all directories given in `path`.
pub fn mkdir_all(opath string, mode u32) -> ! {
if exists(opath) {
Expand Down Expand Up @@ -38,3 +64,58 @@ pub fn mkdir_all(opath string, mode u32) -> ! {
pub fn read_dir(path string) -> ![]string {
return read_dir_iter(path)!.collect(abs_paths: false)
}

// TODO: implement tests and logic to make sure this function is thread-safe.

// remove_dir_recursively removes the directory, specified by path, and all its content.
//
// Function returns an error when:
// - `path` is empty
// - `path` does not exist
// - `path` is not a directory
// - when access to `path` is denied
// - when `force` is `false` and `path` is not empty
//
// The function will try to remove all files, and if any of them fails -
// function will be aborted, and special error [`UnableToRemoveFilesError`] will be returned.
// This error contains list of all files and their reasons why they can't be removed.
//
// If `path` is directory symlink, only it will be removed.
// Original directory will be kept intact.
//
// ATTENTION: there are not confirmation that this function is thread-safe.
// You must make sure that two or more threads don't work on the same `path` at the same time.
//
// Example:
// ```
// fs.remove_dir_recursively('/tmp', true).unwrap()
// ```
//
// To simply remove empty directory use [`remove_empty_dir`].
pub fn remove_dir_recursively(path string) -> ! {
if is_link(path) {
remove(path)!
return
}

files_in_dir := read_dir_iter(path)!
mut files_problems := []UnableToRemoveFileInfo{}

for file in files_in_dir {
abs_file_path := file.path()

if is_dir(abs_file_path) {
remove_dir_recursively(abs_file_path)!
} else {
remove(abs_file_path) or {
files_problems.push(UnableToRemoveFileInfo{ path: abs_file_path, reason: err.msg() })
}
}
}

if files_problems.len > 0 {
return error(UnableToRemoveFilesError{ files_problems: files_problems })
}

remove_empty_dir(path)!
}
127 changes: 127 additions & 0 deletions y/fs/dir_test.sp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module main

import fs
import pathlib
import os

#[run_if(!windows)] // symbolic links are not supported by `is_dir` yet
test "is_dir function" {
Expand Down Expand Up @@ -127,3 +128,129 @@ test "mkdir_all several directories when middle one is already created" {
fs.remove(path).unwrap()
fs.remove(pathlib.dir(path)).unwrap()
}

test "remove_dir_recursively returns error when path is empty" {
fs.remove_dir_recursively("") or {
return
}

t.fail('remove_dir_recursively should fail')
}

test "remove_dir_recursively returns error when path does not exist" {
fs.remove_dir_recursively("/unknown_path") or {
return
}

t.fail('remove_dir_recursively should fail')
}

test "remove_dir_recursively returns error when path is not a directory" {
defer fn () {
fs.remove("./test.txt").unwrap()
}()

fs.write_file("./test.txt", "").unwrap()

fs.remove_dir_recursively("./test.txt") or {
t.assert_eq(fs.exists("./test.txt"), true, 'test.txt must exist')
return
}

t.fail('remove_dir_recursively should fail')
}

test "remove_dir_recursively returns error when access denied" {
defer fn () {
fs.chmod("./TEST", 0o755).unwrap()
fs.remove("./TEST").unwrap()
}()

fs.mkdir("./TEST", 0o000).unwrap()

fs.remove_dir_recursively("./TEST") or {
t.assert_eq(fs.exists("./TEST"), true, './TEST must exist')
return
}

t.fail('remove_dir_recursively should fail')
}

#[run_if(!windows)] // symbolic links are not supported by `is_dir` yet
test "remove_dir_recursively doesn't remove content of original directory if symlink is removed" {
defer fn () {
fs.remove("./TEST/test.txt").unwrap()
fs.remove("./TEST").unwrap()
}()

fs.mkdir("./TEST", 0o755).unwrap()
fs.write_file("./TEST/test.txt", "").unwrap()
fs.symlink("./TEST", "./TEST_SYMLINKED").unwrap()

fs.remove_dir_recursively("./TEST_SYMLINKED").unwrap()

t.assert_eq(fs.exists("./TEST"), true, 'TEST folder must exist')
t.assert_eq(fs.exists("./TEST/test.txt"), true, 'TEST/test.txt must exist')
}

#[run_if(!windows)] // `chattr` only works on Linux
#[run_if(!macos)] // `chattr` only works on Linux

// TODO: this test requires `sudo`, which is a bad idea for unit tests, because password is required.
// Also, this tests can't be just run as root, because root can remove any files.
// This test requires either reimplementation, or a smart trick to be able to run without `sudo`.
#[skip]
test "remove_dir_recursively returns correct error when can't remove inner file" {
defer fn () {
os.exec("sudo chattr -i ./TEST/test.txt").unwrap()
fs.remove("./TEST/test.txt").unwrap()
fs.remove("./TEST").unwrap()
}()

fs.mkdir("./TEST", 0o755).unwrap()
fs.write_file("./TEST/test.txt", "").unwrap()
os.exec("sudo chattr +i ./TEST/test.txt").unwrap()

fs.remove_dir_recursively("./TEST") or {
remove_error := err as fs.UnableToRemoveFilesError

t.assert_eq(remove_error.files_problems.len, 1, 'only one error must exist')
t.assert_eq(remove_error.files_problems[0].path, 'TEST/test.txt', 'path must be correct')
t.assert_eq(remove_error.files_problems[0].reason, 'Cannot remove "TEST/test.txt": Operation not permitted', 'reason must be correct')

t.assert_eq(err.msg(), 'Unable to remove files:\n\tTEST/test.txt: Cannot remove "TEST/test.txt": Operation not permitted', 'correct error must be returned')

t.assert_eq(fs.exists("./TEST"), true, 'TEST folder must exist')
t.assert_eq(fs.exists("./TEST/test.txt"), true, 'TEST/test.txt must exist')

return
}

t.fail('remove_dir_recursively should fail')
}

test "remove_dir_recursively removes empty directory" {
fs.mkdir("./TEST", 0o755).unwrap()
fs.remove_dir_recursively("./TEST").unwrap()
t.assert_eq(fs.exists("./TEST"), false, 'TEST folder must not exist')
}

test "remove_dir_recursively removes directory with files" {
fs.mkdir("./TEST", 0o755).unwrap()
fs.write_file("./TEST/test.txt", "").unwrap()
fs.remove_dir_recursively("./TEST").unwrap()

t.assert_eq(fs.exists("./TEST"), false, 'TEST folder must not exist')
t.assert_eq(fs.exists("./TEST/test.txt"), false, 'TEST/test.txt must not exist')
}

test "remove_dir_recursively removes directory with inner directories with files" {
fs.mkdir("./TEST", 0o755).unwrap()
fs.mkdir("./TEST/inner", 0o755).unwrap()
fs.write_file("./TEST/inner/test.txt", "").unwrap()
fs.remove_dir_recursively("./TEST").unwrap()

t.assert_eq(fs.exists("./TEST"), false, 'TEST folder must not exist')
t.assert_eq(fs.exists("./TEST/inner"), false, 'TEST/inner folder must not exist')
t.assert_eq(fs.exists("./TEST/inner/test.txt"), false, 'TEST/inner/test.txt must not exist')
}
7 changes: 5 additions & 2 deletions y/fs/dir_unix.sp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ pub fn mkdir(path string, mode u32) -> ! {
FsError.throw(libc.mkdir(apath.c_str(), mode) != -1, 'Could not create folder "${path}": ')!
}

// rmdir removes the directory specified by path.
pub fn rmdir(path string) -> ! {
// remove_empty_dir removes the empty directory specified by path.
//
// Function returns an error when the directory is not empty.
// To remove non-empty directory use [`remove_dir_recursively`].
pub fn remove_empty_dir(path string) -> ! {
FsError.throw(libc.rmdir(path.c_str()) != -1, 'Could not remove folder "${path}": ')!
}
7 changes: 5 additions & 2 deletions y/fs/dir_windows.sp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ pub fn mkdir(path string, _ u32) -> ! {
winapi.throw(winapi.CreateDirectoryW(apath.to_wide(), nil), "Failed to create directory '${path}'")!
}

// rmdir removes the directory specified by path.
pub fn rmdir(path string) -> ! {
// remove_empty_dir removes the empty directory specified by path.
//
// Function returns an error when the directory is not empty.
// To remove non-empty directory use [`remove_dir_recursively`].
pub fn remove_empty_dir(path string) -> ! {
winapi.throw(winapi.RemoveDirectoryW(path.to_wide()), "Failed to remove directory '${path}'")!
}