App-FileCleanerByDiskUage

 view release on metacpan or  search on metacpan

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

	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;
	my $freed = 0;

	my $int             = 0;
	my $since_resync    = 0;
	while ( $per >= $opts{du} && $int <= $last_removable ) {
		my $file = $files_info[$int];
		eval {
			if ( $opts{dry_run} ) {
				# dry run: never remove anything, just verify the file would be
				# removable by checking it is writable by the current user
				if ( !-w $file->{name} ) {
					die('file is not writable');
				}
			} else {
				unlink( $file->{name} ) or die($!);
			}

		};
		if ($@) {
			push( @{ $results->{unlink_errors} }, 'Failed to remove "' . $file->{name} . '"... ' . $@ );
			push( @{ $results->{unlink_failed} }, $file );
		} else {
			push( @{ $results->{unlinked} }, $file );
			# a failed unlink frees nothing, so only count successful removals
			$freed += ( $file->{blocks} || 0 ) * 512 unless $opts{dry_run};
		}

		$int++;
		$since_resync++;

		# a dry run never changes disk usage, so re-checking df() would loop
		# forever on the same $per; the index bound above is what stops it.
		next if $opts{dry_run};

		# consult the real df() only once we estimate we have freed enough, or
		# once $resync files have gone by, whichever comes first
		if ( $freed >= $need || $since_resync >= $resync ) {
			$df           = $df_func->($du_path);
			$per          = $df->{per};
			$need         = ( ( $per - $opts{du} ) / 100 ) * $user_total;
			$freed        = 0;
			$since_resync = 0;
		}
	} ## end while ( $per >= $opts{du} && $int <= $last_removable )

	# make sure du_ending reflects real disk usage, not the last estimate
	$df = $df_func->($du_path);
	$results->{du_ending} = $df->{per};
	if ( defined( $results->{unlinked}[0] ) ) {
		$results->{unlinked_count} = $#{ $results->{unlinked} } + 1;
	}
	if ( defined( $results->{unlink_failed}[0] ) ) {
		$results->{unlink_failed_count} = $#{ $results->{unlink_failed} } + 1;
	}



( run in 1.221 second using v1.01-cache-2.11-cpan-7fcb06a456a )