module BBLib # Takes one or more strings and normalizes slashes to create a consistent file path # Useful when concating two strings that when you don't know if one or both will end or begin with a slash def self.pathify(*strings) (strings.first.start_with?('/', '\\') ? strings.first.scan(/^[\/\\]{1,2}/).first : '') + strings.map(&:to_s).msplit('/', '\\').map(&:strip).join('/') end # Scan for files and directories. Can be set to be recursive and can also have filters applied. # @param [String] path The directory to scan files from. # @param [String..., Regexp...] filters A list of filters to apply. Can be regular expressions or strings. # Strings with a * are treated as regular expressions with a .*. If no filters are passed, all files/dirs are returned. # @param [Boolean] recursive When true scan will recursively search directories # @param [Boolean] files If true, paths to files matching the filter will be returned. # @param [Boolean] dirs If true, paths to dirs matching the filter will be returned. # @param [Array] exclude Can be an array of regular expressions or strings that should be ignored when scanning. * in a string is expanded into .*, but all other characters are literal. def self.scan_dir(path, *filters, recursive: false, files: true, dirs: true, exclude: [], filter_base: true, &block) return [] unless Dir.exist?(path) filters = filters.map { |filter| filter.is_a?(Regexp) ? filter : /^#{Regexp.quote(filter).gsub('\\*', '.*')}$/ } exclude = exclude ? [exclude].flatten.map { |exp| exp.is_a?(Regexp) ? exp : /^#{Regexp.quote(exp).gsub('\\*', '.*')}$/ } : [] Dir.foreach(path).flat_map do |item| next if item =~ /^\.{1,2}$/ || (!exclude.empty? && exclude.any? { |exp| item =~ exp }) item = "#{path}/#{item}".gsub('\\', '/') if File.file?(item) if files && (filters.empty? || filters.any? { |filter| item =~ filter || filter_base && item.file_name =~ filter }) block_given? ? yield(item) : item end elsif File.directory?(item) recur = recursive ? scan_dir(item, *filters, recursive: recursive, exclude: exclude, files: files, dirs: dirs, &block) : [] if dirs && (filters.empty? || filters.any? { |filter| item =~ filter || filter_base && item.file_name =~ filter }) (block_given? ? yield(item) : [item] + recur) elsif recursive recur end end end.compact end # Uses BBLib.scan_dir but returns only files def self.scan_files(path, *filters, recursive: false, exclude: [], filter_base: true, &block) scan_dir(path, *filters, recursive: recursive, dirs: false, exclude: exclude, filter_base: filter_base, &block) end # Uses BBLib.scan_dir but returns only directories. def self.scan_dirs(path, *filters, recursive: false, exclude: [], filter_base: true, &block) scan_dir(path, *filters, recursive: recursive, files: false, exclude: exclude, filter_base: filter_base, &block) end # Shorthand method to write a string to disk. By default the # path is created if it doesn't exist. # Set mode to w to truncate file or leave at a to append. def self.string_to_file(str, path, mkpath: true, mode: 'a') FileUtils.mkpath(File.dirname(path)) if mkpath && !Dir.exist?(path) File.write(path, str.to_s, mode: mode) end # A file size parser for strings. Extracts any known patterns for file sizes. def self.parse_file_size(str, output: :byte) output = FILE_SIZES.keys.find { |fs| fs == output || FILE_SIZES[fs][:exp].include?(output.to_s.downcase) } || :byte bytes = 0.0 FILE_SIZES.each do |_k, v| v[:exp].each do |exp| str.scan(/(?=\w|\D|^)\d*\.?\d+\s*#{exp}s?(?=\W|\d|$)/i) .each { |num| bytes += num.to_f * v[:mult] } end end bytes / FILE_SIZES[output][:mult] end # Takes an integer or float and converts it into a string that represents # a file size (e.g. "5 MB 156 kB") # @param [Integer, Float] num The number of bytes to convert to a file size string. # @param [Symbol] input Sets the value of the input. Default is byte. # @param [Symbol] stop Sets a minimum file size to display. # e.g. If stop is set to :megabyte, :kilobyte and below will be truncated. # @param [Symbol] style The out style, Current options are :short and :long def self.to_file_size(num, input: :byte, stop: :byte, style: :short) return nil unless num.is_a?(Numeric) return '0' if num.zero? style = :short unless [:long, :short].include?(style) expression = [] n = num * FILE_SIZES[input.to_sym][:mult] done = false FILE_SIZES.reverse.each do |k, v| next if done done = true if k == stop div = n / v[:mult] next unless div >= 1 val = (done ? div.round : div.floor) expression << "#{val}#{v[:styles][style]}#{val > 1 && style != :short ? 's' : nil}" n -= val.to_f * v[:mult] end expression.join(' ') end FILE_SIZES = { byte: { mult: 1, exp: %w(b byt byte), styles: { short: 'B', long: ' byte' } }, kilobyte: { mult: 1024, exp: %w(kb kilo k kbyte kilobyte), styles: { short: 'kB', long: ' kilobyte' } }, megabyte: { mult: 1024**2, exp: %w(mb mega m mib mbyte megabyte), styles: { short: 'MB', long: ' megabyte' } }, gigabyte: { mult: 1024**3, exp: %w(gb giga g gbyte gigabyte), styles: { short: 'GB', long: ' gigabyte' } }, terabyte: { mult: 1024**4, exp: %w(tb tera t tbyte terabyte), styles: { short: 'TB', long: ' terabyte' } }, petabyte: { mult: 1024**5, exp: %w(pb peta p pbyte petabyte), styles: { short: 'PB', long: ' petabyte' } }, exabyte: { mult: 1024**6, exp: %w(eb exa e ebyte exabyte), styles: { short: 'EB', long: ' exabyte' } }, zettabyte: { mult: 1024**7, exp: %w(zb zetta z zbyte zettabyte), styles: { short: 'ZB', long: ' zettabyte' } }, yottabyte: { mult: 1024**8, exp: %w(yb yotta y ybyte yottabyte), styles: { short: 'YB', long: ' yottabyte' } } }.freeze # Basic detection for whether or not a file is binary or not def self.binary?(file, bytes: 1024, ctrl_threshold: 0.5, binary_threshold: 0.05) ascii = 0 ctrl = 0 binary = 0 read_bytes = File.open(file, 'rb') { |io| io.read(bytes) } return false if read_bytes.nil? || read_bytes.empty? read_bytes.each_byte do |byte| case byte when 0..31 ctrl += 1 when 32..127 ascii += 1 else binary += 1 end end ctrl.to_f / ascii > ctrl_threshold || binary.to_f / ascii > binary_threshold end end # Monkey patches for the Numeric class class Numeric def to_file_size(*args) BBLib.to_file_size(self, *args) end end # Monkey patches for the String class class String def to_file(*args) BBLib.string_to_file(self, *args) end def file_name(with_extension = true) with_extension ? File.basename(self) : File.basename(self, File.extname(self)) end def dirname File.dirname(self) end def parse_file_size(*args) BBLib.parse_file_size(self, *args) end def pathify BBLib.pathify(self) end end