pdfh 0.2.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
-
5
3
  module Pdfh
6
4
  # Handles the config yaml data mapping, and associates a file name with a doc type
7
5
  class Settings
8
- attr_reader :lookup_dirs, :base_path, :document_types
6
+ attr_reader :lookup_dirs, :base_path
9
7
 
10
- # @param config_file [String]
8
+ # @param config_data [Hash]
11
9
  # @return [self]
12
- def initialize(config_file)
13
- file_hash = YAML.load_file(config_file)
14
- Pdfh.verbose_print "Loaded configuration file: #{config_file}"
10
+ def initialize(config_data)
11
+ process_lookup_dirs(config_data[:lookup_dirs])
12
+ process_destination_base(config_data[:destination_base_path])
13
+
14
+ Pdfh.debug "Configured Look up directories:"
15
+ lookup_dirs.each.with_index(1) { |dir, idx| Pdfh.debug " #{idx}. #{dir}" }
16
+ Pdfh.debug
15
17
 
16
- process_lookup_dirs(file_hash["lookup_dirs"])
17
- process_destination_base(file_hash["destination_base_path"])
18
+ load_doc_types(config_data[:document_types])
19
+ end
18
20
 
19
- Pdfh.verbose_print "Configured Look up directories:"
20
- lookup_dirs.each_with_index { |dir, idx| Pdfh.verbose_print " #{idx + 1}. #{dir}" }
21
- Pdfh.verbose_print
21
+ # @return [Array<DocumentType>]
22
+ def document_types
23
+ @document_types.values
24
+ end
22
25
 
23
- @document_types = load_doc_types(file_hash["document_types"])
26
+ # @return [DocumentType]
27
+ def document_type(id)
28
+ @document_types[id]
24
29
  end
25
30
 
26
31
  private
@@ -30,7 +35,7 @@ module Pdfh
30
35
  @lookup_dirs = lookup_dirs_list.filter_map do |dir|
31
36
  expanded = File.expand_path(dir)
32
37
  unless File.directory?(expanded)
33
- Pdfh.verbose_print " ** Error, Directory #{dir} does not exists."
38
+ Pdfh.debug " ** Error, Directory #{dir} does not exists."
34
39
  next
35
40
  end
36
41
  expanded
@@ -47,7 +52,10 @@ module Pdfh
47
52
 
48
53
  # @return [Array<DocumentType>]
49
54
  def load_doc_types(doc_types)
50
- doc_types.map { |data| DocumentType.new(data) }
55
+ @document_types = doc_types.each_with_object({}) do |data, result|
56
+ doc_type = DocumentType.new(data)
57
+ result.store(doc_type.gid, doc_type)
58
+ end
51
59
  end
52
60
  end
53
61
  end
@@ -2,20 +2,20 @@
2
2
 
3
3
  module Pdfh
4
4
  # rubocop:disable Layout/HashAlignment
5
+ DOCUMENT_TYPE_TEMPLATE = {
6
+ "name" => "Example Name",
7
+ "re_file" => ".*file_name\.pdf",
8
+ "re_date" => "(\d{2})\/(?<m>\w+)\/(?<y>\d{4})",
9
+ "pwd" => "BASE64_STRING",
10
+ "store_path" => "{YEAR}/sub folder",
11
+ "name_template" => "{period} {original}",
12
+ "sub_types" => []
13
+ }.freeze
14
+
5
15
  SETTINGS_TEMPLATE = {
6
16
  "lookup_dirs" => ["~/Downloads"].freeze,
7
17
  "destination_base_path" => "~/Documents",
8
- "document_types" => [
9
- {
10
- "name" => "Example Name",
11
- "re_file" => ".*file_name\.pdf",
12
- "re_date" => "(\d{2})\/(?<m>\w+)\/(?<y>\d{4})",
13
- "pwd" => "BASE64_STRING",
14
- "store_path" => "{YEAR}/sub folder",
15
- "name_template" => "{period} {original}",
16
- "sub_types" => []
17
- }.freeze
18
- ].freeze
18
+ "document_types" => [DOCUMENT_TYPE_TEMPLATE].freeze
19
19
  }.freeze
20
20
  # rubocop:enable Layout/HashAlignment
21
21
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfh
4
+ # All console output formats
5
+ class Console
6
+ # @return [self]
7
+ def initialize(verbose)
8
+ @verbose = verbose
9
+ end
10
+
11
+ # @return [void]
12
+ def debug(message = nil)
13
+ msg = message.to_s
14
+ msg = msg.colorize(:cyan) unless msg.colorized?
15
+ output(msg) if verbose?
16
+ end
17
+
18
+ # @return [void]
19
+ def info(message)
20
+ output(message)
21
+ end
22
+
23
+ # Prints visual separator in shell for easier reading for humans
24
+ # @example output
25
+ # ——— Title ——————————————— ... ——————
26
+ # @param title [String]
27
+ # @return [void]
28
+ def headline(title)
29
+ _, cols = console_size
30
+ line_length = cols - (title.size + 5)
31
+ left = ("—" * 3).to_s.red
32
+ right = ("—" * line_length).to_s.red
33
+ output "\n#{left} #{title.colorize(color: :blue, mode: :bold)} #{right}"
34
+ end
35
+
36
+ # @param message [String]
37
+ # @param exit_app [Boolean] exit application if true (default)
38
+ # @return [void]
39
+ def error_print(message, exit_app: true)
40
+ output "Error, #{message}".colorize(:red)
41
+ exit 1 if exit_app
42
+ end
43
+
44
+ # @param message [String]
45
+ # @return [void]
46
+ def warn_print(message)
47
+ output "Warning, #{message}".colorize(:yellow)
48
+ end
49
+
50
+ # @example usage
51
+ # ident_print("Name", "iax")
52
+ # # => Name: "iax"
53
+ # @return [void]
54
+ def ident_print(field, value, color: :green, width: 3)
55
+ field_str = field.to_s.rjust(width)
56
+ value_str = value.colorize(color)
57
+ output "#{" " * 4}#{field_str}: #{value_str}"
58
+ end
59
+
60
+ # Show options used to run the current sync job
61
+ # @param options [Hash]
62
+ # @return [void]
63
+ def print_options(options) # rubocop:disable Metrics/CyclomaticComplexity
64
+ max_size = options.keys.map(&:size).max + 3
65
+ options.each do |key, value|
66
+ left = key.inspect.rjust(max_size).cyan
67
+ right = case value
68
+ when NilClass then value.inspect.colorize(color: :black, mode: :bold)
69
+ when TrueClass then value.inspect.colorize(color: :green, mode: :bold)
70
+ when FalseClass then value.inspect.colorize(color: :red, mode: :bold)
71
+ when Symbol then value.inspect.yellow
72
+ when String then value.inspect.light_magenta
73
+ else
74
+ value.inspect.red
75
+ end
76
+ debug "#{left} => #{right}"
77
+ end
78
+
79
+ nil
80
+ end
81
+
82
+ private
83
+
84
+ # @return [boolean]
85
+ def verbose?
86
+ @verbose
87
+ end
88
+
89
+ # @return [void]
90
+ def output(msg)
91
+ puts(msg)
92
+ end
93
+
94
+ # Returns rows, cols
95
+ # TODO: review https://gist.github.com/nixpulvis/6025433
96
+ # @return [Array<Integer, Integer>]
97
+ def console_size
98
+ `stty size`.split.map(&:to_i)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Pdfh
6
+ # Handles Argument options
7
+ class OptParser
8
+ OPT_PARSER = OptionParser.new do |opts|
9
+ opts.default_argv
10
+ # Process ARGV
11
+ opts.banner = "Usage: #{opts.program_name} [options] [file1 ...]"
12
+ opts.separator ""
13
+ opts.separator "Specific options:"
14
+
15
+ opts.on("-tID", "--type=ID", "Document type id (requires a trailing file list)")
16
+ opts.on_tail("-T", "--list-types", "List document types in configuration") do
17
+ settings = SettingsBuilder.build
18
+ ident = 4
19
+ max_width = settings.document_types.map { |t| t.gid.size }.max
20
+ puts "#{" " * ident}#{"ID".ljust(max_width)} Type Name"
21
+ puts "#{" " * ident}#{"—" * max_width} #{"—" * 23}"
22
+ settings.document_types.each do |type|
23
+ puts "#{" " * ident}#{type.gid.ljust(max_width).yellow} #{type.name.inspect}"
24
+ end
25
+ exit
26
+ end
27
+ opts.on_tail("-V", "--version", "Show version") do
28
+ puts "#{opts.program_name} v#{Pdfh::VERSION}"
29
+ exit
30
+ end
31
+ opts.on_tail("-h", "--help", "help (this dialog)") do
32
+ puts opts
33
+ exit
34
+ end
35
+
36
+ opts.on("-v", "--verbose", "Show more output. Useful for debug")
37
+ opts.on("-d", "--dry", "Dry run, does not write new pdf")
38
+ end
39
+
40
+ class << self
41
+ # @return [Hash]
42
+ def parse_argv
43
+ Pdfh.instance_variable_set(:@console, Console.new(false))
44
+
45
+ options = { dry: false, verbose: false }
46
+ OPT_PARSER.parse!(into: options)
47
+ options[:files] = ARGV if ARGV.any?
48
+ options.transform_keys { |key| key.to_s.tr("-", "_").to_sym }
49
+ rescue OptionParser::InvalidOption => e
50
+ error_print e.message, exit_app: false
51
+ puts OPT_PARSER.help
52
+ exit 1
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfh
4
+ # Argument Options object container
5
+ class Options
6
+ attr_reader :type, :files
7
+
8
+ # @param arg_options [Hash]
9
+ # @return [self]
10
+ def initialize(arg_options)
11
+ @verbose = arg_options[:verbose]
12
+ @dry = arg_options[:dry]
13
+ @type = arg_options[:type]
14
+ @files = arg_options[:files]
15
+ @mode = type ? :file : :directory
16
+ end
17
+
18
+ # @return [Boolean]
19
+ def verbose?
20
+ @verbose
21
+ end
22
+
23
+ # @return [Boolean]
24
+ def dry?
25
+ @dry
26
+ end
27
+
28
+ # @return [Boolean]
29
+ def file_mode?
30
+ @mode == :file
31
+ end
32
+
33
+ # @return [Boolean]
34
+ def files?
35
+ !!@files&.any?
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfh
4
+ # Handles the PDF file
5
+ class PdfFileHandler
6
+ attr_reader :file, :type, :document
7
+
8
+ # @param [String] file
9
+ # @param [DocumentType, nil] type
10
+ # @return [self]
11
+ def initialize(file, type)
12
+ @file = file
13
+ @type = type
14
+ end
15
+
16
+ # @return [boolean]
17
+ def type?
18
+ !!type
19
+ end
20
+
21
+ # Generate document, and process actions
22
+ # @return [void]
23
+ def process_document(base_path)
24
+ Pdfh.info "Working on #{base_name.colorize(:light_green)}"
25
+ raise IOError, "File #{file} not found" unless File.exist?(file)
26
+
27
+ @document = Document.new(file, type, extract_text)
28
+ document.print_info
29
+ write_pdf(base_path)
30
+
31
+ nil
32
+ rescue StandardError => e
33
+ Pdfh.ident_print "Doc Error", e.message, color: :red, width: 12
34
+ end
35
+
36
+ # Create a backup of original document
37
+ # @return [void]
38
+ def make_document_backup(document)
39
+ Pdfh.debug "~~~~~~~~~~~~~~~~~~ Creating PDF backup"
40
+ Dir.chdir(document.home_dir) do
41
+ Pdfh.debug " Working on: #{document.home_dir.inspect} directory"
42
+ Pdfh.debug " mv #{document.file_name.inspect} -> #{document.backup_name.inspect}"
43
+ File.rename(document.file_name, document.backup_name) unless Pdfh.dry?
44
+ end
45
+ end
46
+
47
+ # @return [void]
48
+ def copy_companion_files(destination, document)
49
+ Pdfh.debug "~~~~~~~~~~~~~~~~~~ Writing Companion files"
50
+ document.companion_files.each do |file|
51
+ Pdfh.debug " Working on #{file.inspect}..."
52
+ src_name = File.join(document.home_dir, file)
53
+ src_ext = File.extname(file)
54
+ dest_name = File.basename(document.new_name, ".pdf")
55
+ dest_full = File.join(destination, "#{dest_name}#{src_ext}")
56
+ Pdfh.debug " cp #{src_name} --> #{dest_full}"
57
+ FileUtils.cp(src_name, dest_full) unless Pdfh.dry?
58
+ end
59
+ end
60
+
61
+ # @return [String]
62
+ def base_name
63
+ File.basename(file)
64
+ end
65
+
66
+ private
67
+
68
+ # @return [void]
69
+ def write_pdf(base_path)
70
+ full_path = File.join(base_path, document.store_path, document.new_name)
71
+ dir_path = File.join(base_path, document.store_path)
72
+
73
+ FileUtils.mkdir_p(dir_path)
74
+
75
+ write_new_pdf(dir_path, full_path)
76
+ make_document_backup(document)
77
+ copy_companion_files(dir_path, document)
78
+ rescue StandardError => e
79
+ Pdfh.ident_print "Doc Error", e.message, color: :red, width: IDENT
80
+ end
81
+
82
+ def qpdf_command(*args)
83
+ password_option = type&.password ? "--password=#{type&.password.inspect} " : ""
84
+
85
+ %(qpdf #{password_option}--decrypt #{args.join(" ")})
86
+ end
87
+
88
+ # Gets the text from the pdf in order to execute
89
+ # the regular expresion matches
90
+ # @return [String]
91
+ def extract_text
92
+ temp = Tempfile.new("pdfh")
93
+ Pdfh.debug "~~~~~~~~~~~~~~~~~~ Extract PDF text"
94
+ Pdfh.debug " --> #{temp.path} temporal file assigned."
95
+
96
+ cmd1 = qpdf_command("--stream-data=uncompress", file.inspect, temp.path)
97
+ Pdfh.debug " DeCrypt Command: #{cmd1}"
98
+ _result = `#{cmd1}`
99
+
100
+ cmd2 = %(pdftotext -enc UTF-8 #{temp.path} -)
101
+ Pdfh.debug " Extract Command: #{cmd2}"
102
+ text = `#{cmd2}`
103
+ Pdfh.debug " Text: #{text.inspect}"
104
+ text
105
+ end
106
+
107
+ # @return [void]
108
+ def write_new_pdf(dir_path, full_path)
109
+ Pdfh.debug "~~~~~~~~~~~~~~~~~~ Writing PDFs"
110
+ raise IOError, "Path #{dir_path} not found." unless Dir.exist?(dir_path)
111
+
112
+ cmd = qpdf_command(file.inspect, full_path.inspect)
113
+ Pdfh.debug " Write PDF Command: #{cmd}"
114
+
115
+ return if Pdfh.dry?
116
+
117
+ _result = `#{cmd}`
118
+ raise IOError, "New PDF file #{full_path.inspect} was not created." unless File.file?(full_path)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfh
4
+ # Loads or creates a default settings yaml file
5
+ class SettingsBuilder
6
+ CONFIG_FILE_LOCATIONS = [Dir.pwd, File.expand_path("~")].freeze
7
+ SUPPORTED_EXTENSIONS = %w[yml yaml].freeze
8
+
9
+ class << self
10
+ # @return [Pdfh::Settings]
11
+ def build
12
+ config_file = search_config_file
13
+ file_hash = YAML.load_file(config_file, symbolize_names: true)
14
+ Pdfh.debug "Loaded configuration file: #{config_file}"
15
+
16
+ Settings.new(file_hash)
17
+ end
18
+
19
+ private
20
+
21
+ # @return [String]
22
+ def config_file_name
23
+ File.basename($PROGRAM_NAME)
24
+ end
25
+
26
+ # @return [String (frozen)]
27
+ def default_settings_name
28
+ "#{config_file_name}.#{SUPPORTED_EXTENSIONS.first}"
29
+ end
30
+
31
+ # @return [String]
32
+ def create_settings_file
33
+ full_path = File.join(File.expand_path("~"), default_settings_name)
34
+ return if File.exist?(full_path) # double check
35
+
36
+ File.write(full_path, Pdfh::SETTINGS_TEMPLATE.to_yaml)
37
+ Pdfh.info "Default settings file was created: #{full_path.colorize(:green)}"
38
+
39
+ full_path
40
+ end
41
+
42
+ # Gets the first settings file found, or creates a new one
43
+ # @return [String]
44
+ def search_config_file
45
+ CONFIG_FILE_LOCATIONS.each do |dir|
46
+ SUPPORTED_EXTENSIONS.each do |ext|
47
+ path = File.join(dir, "#{config_file_name}.#{ext}")
48
+ return path if File.exist?(path)
49
+ end
50
+ end
51
+
52
+ Pdfh.warn_print "No configuration file was found within paths: #{CONFIG_FILE_LOCATIONS.join(", ")}"
53
+ create_settings_file
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/pdfh/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfh
4
- VERSION = "0.2.1"
4
+ VERSION = "3.0.1"
5
5
  end
data/lib/pdfh.rb CHANGED
@@ -1,18 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ext/string"
3
+ require "base64"
4
4
  require "colorize"
5
-
6
- require "pdfh/version"
7
- require "pdfh/document_period"
8
- require "pdfh/document_type"
9
- require "pdfh/document"
10
- require "pdfh/month"
11
- require "pdfh/opt_parser"
12
- require "pdfh/pdf_handler"
13
- require "pdfh/settings"
14
- require "pdfh/settings_template"
15
- require "pdfh/document_processor"
5
+ require "fileutils"
6
+ require "forwardable"
7
+ require "tempfile"
8
+ require "yaml"
9
+
10
+ require_relative "ext/string"
11
+
12
+ # Models
13
+ require_relative "pdfh/models/document"
14
+ require_relative "pdfh/models/document_period"
15
+ require_relative "pdfh/models/document_type"
16
+ require_relative "pdfh/models/settings"
17
+
18
+ # Utils
19
+ require_relative "pdfh/utils/console"
20
+ require_relative "pdfh/utils/month"
21
+ require_relative "pdfh/utils/opt_parser"
22
+ require_relative "pdfh/utils/options"
23
+ require_relative "pdfh/utils/pdf_file_handler"
24
+ require_relative "pdfh/utils/settings_builder"
25
+
26
+ require_relative "pdfh/main"
27
+ require_relative "pdfh/settings_template"
28
+ require_relative "pdfh/version"
16
29
 
17
30
  # Gem entry point
18
31
  module Pdfh
@@ -21,117 +34,15 @@ module Pdfh
21
34
 
22
35
  # Regular Date Error, when there is not match
23
36
  class ReDateError < StandardError
37
+ # @return [self]
24
38
  def initialize(msg = "Date regular expression did not find a match in document.")
25
39
  super
26
40
  end
27
41
  end
28
42
 
29
43
  class << self
30
- attr_writer :verbose, :dry, :mode
31
-
32
- # @return [Boolean]
33
- def verbose?
34
- @verbose
35
- end
36
-
37
- # @return [Boolean]
38
- def dry?
39
- @dry
40
- end
41
-
42
- # @return [Boolean]
43
- def file_mode?
44
- @mode == :file
45
- end
46
-
47
- # Returns rows, cols
48
- # TODO: review https://gist.github.com/nixpulvis/6025433
49
- # @return [Array<Integer, Integer>]
50
- def console_size
51
- `stty size`.split.map(&:to_i)
52
- end
53
-
54
- # Prints visual separator in shell for easier reading for humans
55
- # @example output
56
- # [Title Text] -----------------------
57
- # @param msg [String]
58
- # @return [void]
59
- def headline(msg)
60
- _, cols = console_size
61
- line_length = cols - (msg.size + 5)
62
- left = "\033[31m#{"—" * 3}\033[0m"
63
- right = "\033[31m#{"—" * line_length}\033[0m"
64
- puts "\n#{left} \033[1;34m#{msg}\033[0m #{right}"
65
- end
66
-
67
- # @param msg [Object]
68
- # @return [void]
69
- def verbose_print(msg = nil)
70
- puts msg.to_s.colorize(:cyan) if verbose?
71
- end
72
-
73
- # @param message [String]
74
- # @param exit_app [Boolean] exit application if true (default)
75
- # @return [void]
76
- def error_print(message, exit_app: true)
77
- puts "Error, #{message}".colorize(:red)
78
- exit 1 if exit_app
79
- end
80
-
81
- # @param message [String]
82
- # @return [void]
83
- def warn_print(message)
84
- puts message.colorize(:yellow)
85
- end
86
-
87
- # @return [void]
88
- def ident_print(field, value, color: :green, width: 3)
89
- field_str = field.to_s.rjust(width)
90
- value_str = value.colorize(color)
91
- puts "#{" " * 4}#{field_str}: #{value_str}"
92
- end
93
-
94
- # @return [Hash]
95
- def parse_argv
96
- options = {}
97
- OPT_PARSER.parse!(into: options)
98
- options[:files] = ARGV if ARGV.any?
99
- options.transform_keys { |key| key.to_s.tr("-", "_").to_sym }
100
- rescue OptionParser::InvalidOption => e
101
- error_print e.message, exit_app: false
102
- puts OPT_PARSER.help
103
- exit 1
104
- end
105
-
106
- # @return [String]
107
- def config_file_name
108
- File.basename($PROGRAM_NAME)
109
- end
110
-
111
- # @return [void]
112
- def create_settings_file
113
- full_path = File.join(File.expand_path("~"), "#{config_file_name}.yml")
114
- return if File.exist?(full_path) # double check
115
-
116
- File.write(full_path, Pdfh::SETTINGS_TEMPLATE.to_yaml)
117
- puts "Settings #{full_path.inspect.colorize(:green)} was created."
118
- end
119
-
120
- # @raise [SettingsIOError] if no file is found
121
- # @return [String]
122
- def search_config_file
123
- names_to_look = %W[#{config_file_name}.yml #{config_file_name}.yaml]
124
- dir_order = [Dir.pwd, File.expand_path("~")]
125
-
126
- dir_order.each do |dir|
127
- names_to_look.each do |file|
128
- path = File.join(dir, file)
129
- return path if File.exist?(path)
130
- end
131
- end
132
-
133
- raise SettingsIOError, "no configuration file (#{names_to_look.join(" or ")}) was found\n"\
134
- " within paths: #{dir_order.join(", ")}"
135
- end
44
+ extend Forwardable
45
+ def_delegators :@options, :verbose?, :dry?, :file_mode?
46
+ def_delegators :@console, :ident_print, :warn_print, :error_print, :headline, :debug, :info, :print_options
136
47
  end
137
48
  end
data/pdfh.gemspec CHANGED
@@ -10,11 +10,11 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["iax7@users.noreply.github.com"]
11
11
 
12
12
  spec.summary = "Organize PDF files"
13
- spec.description = "Examine all PDF files in Look up directories, remove password (if has one), "\
13
+ spec.description = "Examine all PDF files in Look up directories, remove password (if has one), " \
14
14
  "rename and copy to a new directory using regular expressions."
15
15
  spec.homepage = "https://github.com/iax7/pdfh"
16
16
  spec.license = "MIT"
17
- spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
17
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
18
18
 
19
19
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
20