App-FileCleanerByDiskUage

 view release on metacpan or  search on metacpan

lib/App/FileCleanerByDiskUage.pm  view on Meta::CPAN

	}

	if ( !$opts{dry_run} ) {
		$opts{dry_run} = 0,;
	} else {
		$opts{dry_run} = 1,;
	}

	# df() is normally Filesys::Df::df, but may be overridden internally via the
	# _df option so the removal loop's disk-usage logic can be tested without a
	# real filesystem. _resync bounds how many files may be removed between real
	# df() checks (see the removal loop below).
	my $df_func = ( ref( $opts{_df} ) eq 'CODE' ) ? $opts{_df} : \&df;
	my $resync = ( defined( $opts{_resync} ) && $opts{_resync} =~ /^\d+$/ && $opts{_resync} > 0 ) ? $opts{_resync} : 64;

	my $df = $df_func->($du_path);

	# the results to be returned
	my $results = {
		unlinked            => [],
		unlink_failed       => [],
		unlink_errors       => [],
		found_files         => [],
		found_files_count   => 0,
		unlinked_count      => 0,
		unlink_failed_count => 0,
		du_target           => $opts{du},
		du_starting         => $df->{per},
		du_ending           => $df->{per},
		min_files           => 0,
		dry_run             => $opts{dry_run},
		path                => \@paths,
		missing_paths       => \@missing_paths,
	};

	if ( !defined( $paths[0] ) ) {
		if ( $opts{use_pid} ) {
			unlink_pid_file($pid_file);
		}
		return $results;
	}

	if ( $df->{per} < $opts{du} ) {
		if ( $opts{use_pid} ) {
			unlink_pid_file($pid_file);
		}
		return $results;
	}

	# compile the ignore regexp once, if specified, and reuse it below
	my $ignore_re = defined( $opts{ignore} ) ? qr/$opts{ignore}/ : undef;

	# Recursively find regular files under the requested paths, statting each
	# one inline during the traversal. Doing the stat here means a single stat
	# syscall per file (the traversal and the mtime lookup share it) and avoids
	# building a separate array of path strings alongside the file info.
	my @files_info;
	File::Find::find(
		sub {
			# $_ is the basename (we are chdir'd into the containing dir),
			# $File::Find::name is the full path. stat($_) populates the "_"
			# handle so the -f test below reuses it rather than statting again.
			my @stat = stat($_);
			return unless @stat;    # skip on stat failure (races, broken symlinks)
			return unless -f _;     # regular files only, matching the old ->file rule
			return if defined($ignore_re) && $_ =~ $ignore_re;    # ignore by basename
			# blocks ($stat[12], 512-byte units) is the space actually freed by
			# unlinking, used by the removal loop to estimate disk usage between
			# df() calls. apparent size would over-count sparse/small files.
			push( @files_info, { name => $File::Find::name, mtime => $stat[9], blocks => $stat[12] } );
		},
		@paths
	);
	$results->{found_files_count} = scalar(@files_info);

	# if we have a min number of files specified, make sure we found more than
	# that many. min_files elements at indexes 0 .. min_files-1, so index
	# min_files existing means there is at least one file eligible for removal.
	if ( $opts{min_files} && !defined( $files_info[ $opts{min_files} ] ) ) {
		$results->{min_files} = $opts{min_files};
		if ( $opts{use_pid} ) {
			unlink_pid_file($pid_file);
		}
		return $results;
	}

	# sort files oldest to newest based on mtime, numerically
	@files_info = sort { $a->{mtime} <=> $b->{mtime} } @files_info;
	# save the full, sorted list into the results; the unlink loop below is
	# bounded by an index so it never touches this array, meaning we don't
	# need a defensive copy here
	$results->{found_files} = \@files_info;

	# the newest min_files files are kept regardless of disk usage. As the list
	# is sorted oldest to newest, those are the last min_files entries, so we
	# simply stop the removal loop before reaching them rather than removing
	# them from the array.
	my $min_files = 0;
	if ( defined( $opts{min_files} ) && $opts{min_files} > 0 ) {
		$min_files            = $opts{min_files};
		$results->{min_files} = $min_files;
	}
	# index of the last (oldest end) file eligible for removal
	my $last_removable = $#files_info - $min_files;

	# go through files and remove the oldest till we drop below the threshold.
	#
	# Rather than calling df() after every single unlink (a statvfs syscall each
	# time, which dominates the loop on high latency filesystems), we estimate
	# how much space we still need to free from the block counts we already have
	# and only consult the real df() when the estimate says we should be close.
	# The real df() remains the authoritative stop condition, so this never
	# under removes; the $resync cap bounds how far a bad estimate (concurrent
	# writers, files held open elsewhere) can run us past the target.
	my $per = $df->{per};

	# bytes the user may occupy, used to translate a percentage into bytes. The
	# byte mode df() (block size of 1) reports used/bavail in bytes.
	my $df_bytes = $df_func->( $du_path, 1 );
	my $user_total = ( $df_bytes->{used} || 0 ) + ( $df_bytes->{bavail} || 0 );
	# estimated bytes still to free to reach the target, and bytes freed since
	# the last real df() check
	my $need  = ( ( $per - $opts{du} ) / 100 ) * $user_total;



( run in 0.607 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )