The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Evo::Fs

VERSION

version 0.0259

SYNOPSIS

  # single
  use Evo '-Fs FS';
  say FS->ls('./');


  # class
  use Evo '-Fs; File::Basename fileparse';
  my $orig_fs = Evo::Fs->new;
  my $fs      = $orig_fs->cd('/tmp');    # new Fs with cwd as '/tmp'

  my $fh = $fs->open('foo/bar.txt', 'w');    # open and create '/foo' if necessary
  $fs->close($fh);

  $fs->write('a/foo', 'one');
  $fs->append('a/foo', 'two');
  say $fs->read('a/foo');                # onetwo
  say $fs->read('/tmp/a/foo');           # the same, a/foo resolves to /tmp/a/foo
                                         # bulk
                                         

  $fs->write_many('/tmp/a/foo' => 'afoo', '/tmp/b/foo' => 'bfoo');
  $fs->sysopen($fh, '/tmp/c', 'w+');
  $fs->syswrite($fh, "123456");
  $fs->sysseek($fh, 0);
  $fs->sysread($fh, \my $buf, 3);
  say $buf;                              # 123
  $fs->find_files(

    # where to start
    './',

    # do something with file
    sub ($path) {
      say $path;
    },

    # skip dirs like .git
    sub ($path) {
      scalar fileparse($path) !~ /^\./;
    }
  );
  $fs->find_files(
    ['/tmp'],
    sub ($path, $stat) {
      say $path;
    }
  );

DESCRIPTION

An abstraction layer between file system and your application. Provides a nice interface for blocking I/O and other file stuff.

It's worth to use at least because allow you to test FS logic of your app with the help of Evo::Fs::Class::Temp.

Imagine, you have an app that should read /etc/passwd and validate a user validate_user. To test this behaviour with traditional IO you should implement read_passwd operation and stub it. With Evo::Fs you can just create a temporary filesystem with chroot like behaviour, fill /etc/passwd and inject this as a dependency to you app:

Here is our app. Pay attention it has a fs attribute with default.

  package My::App;
  use Evo '-Fs FS; -Class';

  has fs => sub { FS() };

  sub validate_user ($self, $user) {
    $self->fs->read('/etc/passwd') =~ /$user/;
  }

And here is how we test it

  package main;
  use Evo '-Fs; -Fs::Temp; Test::More';
  my $app = My::App->new(fs => Evo::Fs::Temp->new);    # provide dependency as Evo::Fs::Class::Temp

  # or mock the single object
  local $Evo::Fs::SINGLE = Evo::Fs::Temp->new;
  $app = My::App->new();                               # provide dependency as Evo::Fs::Class::Temp

  $app->fs->write('/etc/passwd', 'alexbyk:x:1:1');
  diag "Root is: " . $app->fs->root;                   # temporary fs has a "root" method

  ok $app->validate_user('alexbyk');
  ok !$app->validate_user('not_existing');

  done_testing;

We created a temporary FileSystem and passed it as fs attribute. Now we can write /etc/passwd file in chrooted envirement. This testing strategy is simple and good.

You can also mock a single object this way

  local $Evo::Fs::SINGLE = Evo::Fs::Temp->new;
  say FS();

EXPORTS

FS, $Evo::Fs::SINGLE

Return a single instance of Evo::Fs

METHODS

sysopen ($self, $path, $mode, $perm=...)

  my $fh = $fs->open('/foo/bar.txt', 'w');

Open a file and return a filehandle. Create parent directories if necessary. See "sysopen" for list of modes

cd ($self, $path)

  my $new = $fs->cd('foo/bar');
  say $new->cwd;    # ~/foo/bar
  $new = $fs->cd('foo/bar');
  say $new->cwd;    # ~/foo/bar

Returns new FS with passed cwd

cdm ($self, $path)

Same as "cd" but also calls "make_tree" before

append, write, read, read_ref

  $fs->write('/tmp/my/file', 'foo');
  $fs->append('/tmp/my/file', 'bar');
  say $fs->read('/tmp/my/file');            # foobar
  say $fs->read_ref('/tmp/my/file')->$*;    # foobar

Read, write or append a content to the file. Dirs will be created if they don't exist. Use lock 'ex' for append and write and lock 'sh' for read during each invocation

write_many

Write many files using write

sysseek($self, $position, $whence='start')

Whence can be one of:

sysread ($self, $fh, $ref, $length[, $offset])

Call sysread but accepts scalar reference for convinience

syswrite($self, $fh, $scalar, $length, $offset)

Call syswrite

sysopen ($self, $fh, $path, $mode, $perm=...)

  $fs->sysopen(my $fh, '/tmp/foo', 'r');

Mode can be one of:

* w Open file for writing. The file is created (if it does not exist) or truncated (if it exists). * wx Like w but fails if path exists. * w+ Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists). * wx+ Like w+ but fails if path exists.

* a Open file for appending. The file is created if it does not exist. * ax Like a but fails if path exists. * a+ Open file for reading and appending. The file is created if it does not exist. * ax+ Like a+ but fails if path exists.

rename($self, $oldpath, $newpath)

Rename a file.

stat($self, $path)

Return a Evo::Fs::Stat object

to_abs

  my $fs = Evo::Fs::Base->new(cwd => '/foo');
  say $fs->to_abs('bar');    # /foo/bar

Convert relative path to absolute, depending on "cwd" attribute. This is virtual represantion only and "root" doesn't affects the value

cwd

Current working directory which affects relative paths. Should be absolute.

root

Can be used like a chroot in Linux. Should be absolute

path2real($virtual)

Convert a virtual path to the real one.

find_files($self, $dirs, $fn, $pick=undef)

  $fs->find_files('./tmp', sub ($fh) {...}, sub ($dir) {...});
  $fs->find_files(['/tmp'], sub ($fh) {...});

Find files in given directories. You can skip some directories by providing $pick->($dir) function. This will work ok on circular links, hard links and so on. Every path will be passed to $fn->($fh)only once even if it has many links.

So, in situations, when a file have several hard and symbolic links, only one of them will be passed to $fn, and potentially each time it can be different path for each find_files invocation.

See "traverse" for examining all nodes. This method just decorate it's arguments

SKIP_HIDDEN

You can also traverse all files, but ignore hidden directories, like ".git" this way:

  use Evo '-Fs FS SKIP_HIDDEN';
  FS->find_files('./', sub($path) { say $path; }, SKIP_HIDDEN)

traverse($self, $dirs, $fn, $pick=undef)

Traverse directories and invoke $fn->$path for each child node.

Each file is processed only once no matter how many links it has. So instead of a real filename you may be getting a link and never a real name depending on which one (file or link) was met first

You can provide $pick->($dir) to skip directories, for example, to skip hidden ones. By default all directories are processed

  $fs->traverse('/tmp', sub ($path) {...}, sub ($dir) {...});
  $fs->traverse(['/tmp'], sub ($path) {...},);

Also this method doesn't try to access directories without X and R permissions or pass them to $pick (but such directories will be passed to fn because are regular nodes)

In most cases you may want to use "find_files" instead.

AUTHOR

alexbyk.com

COPYRIGHT AND LICENSE

This software is copyright (c) 2016 by alexbyk.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.