File-Trash-FreeDesktop

 view release on metacpan or  search on metacpan

lib/File/Trash/FreeDesktop.pm  view on Meta::CPAN

package File::Trash::FreeDesktop;

use 5.010001;
use strict;
use warnings;
use Log::ger;

use Fcntl;
use File::Util::Test qw(file_exists l_abs_path);

our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
our $DATE = '2023-11-21'; # DATE
our $DIST = 'File-Trash-FreeDesktop'; # DIST
our $VERSION = '0.207'; # VERSION

sub new {
    require File::HomeDir::FreeDesktop;

    my ($class, %opts) = @_;

    my $home = File::HomeDir::FreeDesktop->my_home
        or die "Can't get homedir, ".
            "probably not a freedesktop-compliant environment?";
    $opts{_home} = l_abs_path($home);

    bless \%opts, $class;
}

sub _mk_trash {
    my ($self, $trash_dir) = @_;
    for ("", "/files", "/info") {
        my $d = "$trash_dir$_";
        unless (-d $d) {
            log_trace("Creating directory %s ...", $d);
            mkdir $d, 0700 or die "Can't mkdir $d: $!";
        }
    }
}

sub _home_trash {
    my ($self) = @_;
    "$self->{_home}/.local/share/Trash";
}

sub _mk_home_trash {
    my ($self) = @_;
    for (".local", ".local/share") {
        my $d = "$self->{_home}/$_";
        unless (-d $d) {
            mkdir $d or die "Can't mkdir $d: $!";
        }
    }
    $self->_mk_trash("$self->{_home}/.local/share/Trash");
}

sub _select_trash {
    require Sys::Filesystem::MountPoint;

    my ($self, $file0) = @_;
    file_exists($file0) or die "File doesn't exist: $file0";
    my $afile = l_abs_path($file0);

    # since path_to_mount_point resolves symlink (sigh), we need to remove the
    # leaf. otherwise: /mnt/sym -> / will cause mount point to become / instead
    # of /mnt
    my $afile2 = $afile; $afile2 =~ s!/[^/]+\z!! if (-l $file0);
    my $file_mp = Sys::Filesystem::MountPoint::path_to_mount_point($afile2);

    if ($ENV{PERL_FILE_TRASH_FREEDESKTOP_DEBUG}) {
        log_trace "File's mountpoint for file $file0 is $file_mp";
    }

    $self->{_home_mp} //= Sys::Filesystem::MountPoint::path_to_mount_point(
        $self->{_home});

    if ($ENV{PERL_FILE_TRASH_FREEDESKTOP_DEBUG}) {
        log_trace "Home mountpoint for file $file0 is $self->{_home_mp}";
    }

    # try home trash
    if ($self->{_home_mp} eq $file_mp) {
        my $trash_dir = $self->_home_trash;
        log_trace("Selected home trash for %s = %s", $afile, $trash_dir);

lib/File/Trash/FreeDesktop.pm  view on Meta::CPAN


    $self->_erase({}, $trash_dir);
}

1;
# ABSTRACT: Trash files

__END__

=pod

=encoding UTF-8

=head1 NAME

File::Trash::FreeDesktop - Trash files

=head1 VERSION

This document describes version 0.207 of File::Trash::FreeDesktop (from Perl distribution File-Trash-FreeDesktop), released on 2023-11-21.

=head1 SYNOPSIS

 use File::Trash::FreeDesktop;

 my $trash = File::Trash::FreeDesktop->new;

 # list available (for the running user) trash directories
 my @trashes = $trash->list_trashes;

 # list the content of a trash directory
 my @content = $trash->list_contents("/tmp/.Trash-1000");

 # list the content of all available trash directories
 my @content = $trash->list_contents;

 # trash a file
 $trash->trash("/foo/bar");

 # specify some options when trashing
 $trash->trash({on_not_found=>'ignore'}, "/foo/bar");

 # recover a file from trash (untrash)
 $trash->recover('/foo/bar');

 # untrash a file from a specific trash directory
 $trash->recover('/tmp/file', '/tmp/.Trash-1000');

 # specify some options when untrashing
 $trash->recover({on_not_found=>'ignore', on_target_exists=>'ignore'}, '/path');

 # empty a trash directory
 $trash->empty("$ENV{HOME}/.local/share/Trash");

 # empty all available trashes
 $trash->empty;

=head1 DESCRIPTION

This module lets you trash/erase/restore files, also list the contents of trash
directories. This module follows the freedesktop.org trash specification [1],
with some notes/caveats:

=over

=item * For home trash, $HOME/.local/share/Trash is used instead of $HOME/.Trash

This is what KDE and GNOME use these days.

=item * Symlinks are currently not checked

The spec requires implementation to check whether trash directory is a symlink,
and refuse to use it in that case. This module currently does not do said
checking.

=item * Currently cross-device copying is not implemented/done

It should not matter though, because trash directories are per-filesystem.

=back

Keywords: recycle bin

=head1 THE TRASH STRUCTURE

The following is a short description of the trash structure.

A trash directory is a per-filesystem, per-user directory structure to allow
files to be "trashed", i.e. to be put inside and to be recovered to its original
location later should a user changes his/her mind and wants the files back.
Otherwise, user can "empty" the trash to delete files permanently.

A trash directory, e.g. C</home/USER1/.local/share/Trash>, contains two
subdirectories: C<info> and C<files>. The C<files> contain the actual trashed
files and their name must be unique. Thus if C</home/USER1/foo.txt> is trashed
and then another C</home/USER1/foo.txt> is trashed again, the second file must
be renamed to C</home/USER1/foo (1).txt> or something else.

The C<info> subdirectory contains the metadata for each trashed file, with each
metadata put in an INI of the same name of the correspoonding file in C<files>
with C<.trashinfo> suffix, under the INI C<Trash Info> section. Known INI
parameters include: C<Path> (the original name/path of the trashed file) and
C<DeletionDate> (date and time, in ISO8601 format).

=head1 NOTES

Weird scenario: /PATH/.Trash-UID is mounted on its own scenario? How about
/PATH/.Trash-UID/{files,info}.

=head1 METHODS

=head2 $trash = File::Trash::FreeDesktop->new(%opts)

Constructor.

Known options:

=over

=item * home_only

lib/File/Trash/FreeDesktop.pm  view on Meta::CPAN


=item * mtime => INT

Only recover file if file's mtime is the one specified. This can be useful to
make sure that the file we recover is really the one that we trashed earlier,
especially if we trash several files with the same path.

(Ideally, instead of mtime we should use some unique ID that we write in the
.trashinfo file, but I fear that an extra parameter in .trashinfo file might
confuse other implementations.)

See also C<suffix>, which is the recommended way to identify and recover
particular file.

=item * suffix => STR

Only recover file having the specified suffix, chosen previously during trash().

=back

=head2 $trash->erase([ \%opts, ] $file[, $trash_dir]) => LIST

Erase (unlink()) a file or multiple files in trash.

Unless C<$trash_dir> is specified, will empty all existing user's trash dirs.
Will ignore if file does not exist in trash. Will die on errors.

To erase multiple files based on wilcard or regexp pattern, use the options. See
C<list_contents()>.

Return list of files erased.

=head2 $trash->empty([$trash_dir]) => LIST

Empty trash.

Unless $trash_dir is specified, will empty all existing user's trash dirs. Will
die on errors.

Return list of files erased.

=head1 ENVIRONMENT

=head2 PERL_FILE_TRASH_FREEDESKTOP_DEBUG

Bool, if set to true will produce additional logging statements using
L<Log::ger> at the C<trace> level.

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/File-Trash-FreeDesktop>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-File-Trash-FreeDesktop>.

=head1 SEE ALSO

=head2 Specification

L<https://freedesktop.org/wiki/Specifications/trash-spec>

=head2 CLI utilities

=over

=item * App::TrashUtils

A set of CLI's written in Perl: L<trash-empty>, L<trash-list>,
L<trash-list-trashes>, L<trash-put>, L<trash-restore>, L<trash-rm>.

=item * L<trash-u> (from App::trash::u)

An alternative CLI, with undo support.

=item * trash-cli

A set of CLI's written in Python: C<trash-empty>, C<trash-list>, C<trash-put>,
C<trash-restore>, C<trash-rm>.

L<https://github.com/andreafrancia/trash-cli>

=back

=head2 Related CPAN modules

=over

=item * L<Trash::Park>

Different trash structure (a single CSV file per trash to hold a list of deleted
files, files stored using original path structure, e.g. C<home/dir/file>). Does
not create per-filesystem trash.

=item * L<File::Trash>

Different trash structure (does not keep info file, files stored using original
path structure, e.g. C<home/dir/file>). Does not create per-filesystem trash.

=item * L<File::Remove>

File::Remove includes the trash() function which supports Win32, but no
undeletion function is provided at the time of this writing.

=back

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 CONTRIBUTOR

=for stopwords Steven Haryanto

Steven Haryanto <stevenharyanto@gmail.com>

=head1 CONTRIBUTING


To contribute, you can send patches by email/via RT, or send pull requests on
GitHub.



( run in 0.461 second using v1.01-cache-2.11-cpan-e1769b4cff6 )