timet 1.6.1.1 → 1.6.3

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.
@@ -3,51 +3,41 @@
3
3
  module Timet
4
4
  # Module responsible for synchronizing local and remote databases
5
5
  module DatabaseSyncer
6
- # Fields used in item operations
7
6
  ITEM_FIELDS = %w[start end tag notes pomodoro updated_at created_at deleted].freeze
8
7
 
9
- # Handles the synchronization process when differences are detected between databases
10
- #
11
- # @param local_db [SQLite3::Database] The local database connection
12
- # @param remote_storage [S3Supabase] The remote storage client for cloud operations
13
- # @param bucket [String] The S3 bucket name
14
- # @param local_db_path [String] Path to the local database file
15
- # @param remote_path [String] Path to the downloaded remote database file
16
- # @return [void]
17
- # @note This method attempts to sync the databases and handles any errors that occur during the process
18
8
  def handle_database_differences(*args)
19
9
  local_db, remote_storage, bucket, local_db_path, remote_path = args
20
10
  puts 'Differences detected between local and remote databases'
21
11
  begin
22
12
  sync_with_remote_database(local_db, remote_path, remote_storage, bucket, local_db_path)
23
13
  rescue SQLite3::Exception => e
24
- handle_sync_error(e, remote_storage, bucket, local_db_path)
14
+ handle_sync_error(e, remote_storage: remote_storage, bucket: bucket, local_db_path: local_db_path)
25
15
  end
26
16
  end
27
17
 
28
- # Handles errors that occur during database synchronization
29
- #
30
- # @param error [SQLite3::Exception] The error that occurred during sync
31
- # @param remote_storage [S3Supabase] The remote storage client for cloud operations
32
- # @param bucket [String] The S3 bucket name
33
- # @param local_db_path [String] Path to the local database file
34
- # @return [void]
35
- # @note When sync fails, this method falls back to uploading the local database
36
- def handle_sync_error(error, remote_storage, bucket, local_db_path)
18
+ def handle_sync_error(error, *args)
19
+ first_arg = args.first
20
+ if first_arg.is_a?(Hash)
21
+ options = first_arg
22
+ remote_storage, bucket, local_db_path = options.values_at(:remote_storage, :bucket, :local_db_path)
23
+ else
24
+ remote_storage, bucket, local_db_path = args
25
+ end
26
+ report_sync_error(error)
27
+ upload_local_database(remote_storage, bucket, local_db_path)
28
+ end
29
+
30
+ def report_sync_error(error)
37
31
  puts "Error opening remote database: #{error.message}"
38
32
  puts 'Uploading local database to replace corrupted remote database'
33
+ end
34
+ module_function :report_sync_error
35
+
36
+ def upload_local_database(remote_storage, bucket, local_db_path)
39
37
  remote_storage.upload_file(bucket, local_db_path, 'timet.db')
40
38
  end
39
+ module_function :upload_local_database
41
40
 
42
- # Performs the actual database synchronization by setting up connections and syncing data
43
- #
44
- # @param local_db [SQLite3::Database] The local database connection
45
- # @param remote_path [String] Path to the remote database file
46
- # @param remote_storage [S3Supabase] The remote storage client for cloud operations
47
- # @param bucket [String] The S3 bucket name
48
- # @param local_db_path [String] Path to the local database file
49
- # @return [void]
50
- # @note Configures both databases to return results as hashes for consistent data handling
51
41
  def sync_with_remote_database(*args)
52
42
  local_db, remote_path, remote_storage, bucket, local_db_path = args
53
43
  db_remote = open_remote_database(remote_path)
@@ -56,12 +46,6 @@ module Timet
56
46
  sync_databases(local_db, db_remote, remote_storage, bucket, local_db_path)
57
47
  end
58
48
 
59
- # Opens and validates a connection to the remote database
60
- #
61
- # @param remote_path [String] Path to the remote database file
62
- # @return [SQLite3::Database] The initialized database connection
63
- # @raise [RuntimeError] If the database connection cannot be established
64
- # @note Validates that the database connection is properly initialized
65
49
  def open_remote_database(remote_path)
66
50
  db_remote = SQLite3::Database.new(remote_path)
67
51
  raise 'Failed to initialize remote database' unless db_remote
@@ -69,15 +53,6 @@ module Timet
69
53
  db_remote
70
54
  end
71
55
 
72
- # Synchronizes the local and remote databases by comparing and merging their items
73
- #
74
- # @param local_db [SQLite3::Database] The local database connection
75
- # @param remote_db [SQLite3::Database] The remote database connection
76
- # @param remote_storage [S3Supabase] The remote storage client for cloud operations
77
- # @param bucket [String] The S3 bucket name
78
- # @param local_db_path [String] Path to the local database file
79
- # @return [void]
80
- # @note This method orchestrates the entire database synchronization process
81
56
  def sync_databases(*args)
82
57
  local_db, remote_db, remote_storage, bucket, local_db_path = args
83
58
  process_database_items(local_db, remote_db)
@@ -85,130 +60,117 @@ module Timet
85
60
  puts 'Database sync completed'
86
61
  end
87
62
 
88
- # Processes items from both databases and syncs them
89
- #
90
- # @param local_db [SQLite3::Database] The local database connection
91
- # @param remote_db [SQLite3::Database] The remote database connection
92
- # @return [void]
93
63
  def process_database_items(local_db, remote_db)
94
64
  remote_items = remote_db.execute('SELECT * FROM items ORDER BY updated_at DESC')
95
65
  local_items = local_db.execute_sql('SELECT * FROM items ORDER BY updated_at DESC')
96
66
 
97
- sync_items_by_id(
98
- local_db,
99
- items_to_hash(local_items),
100
- items_to_hash(remote_items)
101
- )
67
+ sync_items_by_id(local_db, items_to_hash(local_items), items_to_hash(remote_items))
102
68
  end
103
69
 
104
- # Syncs items between local and remote databases based on their IDs
105
- #
106
- # @param local_db [SQLite3::Database] The local database connection
107
- # @param local_items_by_id [Hash] Local items indexed by ID
108
- # @param remote_items_by_id [Hash] Remote items indexed by ID
109
- # @return [void]
110
70
  def sync_items_by_id(local_db, local_items_by_id, remote_items_by_id)
111
71
  all_item_ids = (remote_items_by_id.keys + local_items_by_id.keys).uniq
72
+ all_item_ids.each { |id| sync_single_item(local_db, id, local_items_by_id, remote_items_by_id) }
73
+ end
112
74
 
113
- all_item_ids.each do |id|
114
- if !remote_items_by_id[id]
115
- puts "Local item #{id} will be uploaded"
116
- elsif !local_items_by_id[id]
117
- puts "Adding remote item #{id} to local"
118
- insert_item_from_hash(local_db, remote_items_by_id[id])
119
- else
120
- process_existing_item(id, local_items_by_id[id], remote_items_by_id[id], local_db)
121
- end
75
+ def sync_single_item(*args)
76
+ local_db, id, local_items_by_id, remote_items_by_id = args
77
+ remote_item = remote_items_by_id[id]
78
+ local_item = local_items_by_id[id]
79
+
80
+ if !remote_item
81
+ log_local_only(id)
82
+ elsif !local_item
83
+ add_remote_item(local_db, id, remote_item)
84
+ else
85
+ merge_item(local_db, id, local_item, remote_item)
122
86
  end
123
87
  end
124
88
 
125
- # Inserts a new item into the database from a hash
126
- #
127
- # @param db [SQLite3::Database] The database connection
128
- # @param item [Hash] Hash containing item data
129
- # @return [void]
89
+ def log_local_only(id)
90
+ puts "Local item #{id} will be uploaded"
91
+ end
92
+ module_function :log_local_only
93
+
94
+ def add_remote_item(local_db, id, remote_item)
95
+ puts "Adding remote item #{id} to local"
96
+ insert_item_from_hash(local_db, remote_item)
97
+ end
98
+
99
+ def process_existing_item(id, local_item, remote_item, local_db)
100
+ merge_item(local_db, id, local_item, remote_item)
101
+ end
102
+
103
+ def merge_item(*args)
104
+ local_db, id, local_item, remote_item = args
105
+ local_time = extract_timestamp(local_item)
106
+ remote_time = extract_timestamp(remote_item)
107
+
108
+ return resolve_remote_wins(local_db, id, remote_item) if remote_wins?(remote_item, remote_time, local_time)
109
+
110
+ log_local_wins(id, local_item)
111
+ end
112
+
113
+ def resolve_remote_wins(local_db, id, remote_item)
114
+ puts format_status_message(id, remote_item, 'Remote')
115
+ update_item_from_hash(local_db, remote_item)
116
+ :local_update
117
+ end
118
+
119
+ def log_local_wins(id, local_item)
120
+ puts format_status_message(id, local_item, 'Local')
121
+ :remote_update
122
+ end
123
+
130
124
  def insert_item_from_hash(db, item)
131
125
  fields = ['id', *ITEM_FIELDS].join(', ')
132
126
  placeholders = Array.new(ITEM_FIELDS.length + 1, '?').join(', ')
133
127
  db.execute_sql(
134
128
  "INSERT INTO items (#{fields}) VALUES (#{placeholders})",
135
- get_item_values(item, include_id_at_start: true)
129
+ get_insert_values(item)
136
130
  )
137
131
  end
138
132
 
139
- # Processes an item that exists in both databases
140
- #
141
- # @param id [Integer] Item ID
142
- # @param local_item [Hash] Local database item
143
- # @param remote_item [Hash] Remote database item
144
- # @param local_db [SQLite3::Database] Local database connection
145
- # @return [Symbol] :local_update if local was updated, :remote_update if remote needs update
146
- def process_existing_item(*args)
147
- id, local_item, remote_item, local_db = args
148
- local_time = local_item['updated_at'].to_i
149
- remote_time = remote_item['updated_at'].to_i
150
-
151
- if remote_wins?(remote_item, remote_time, local_time)
152
- puts format_status_message(id, remote_item, 'Remote')
153
- update_item_from_hash(local_db, remote_item)
154
- :local_update
155
- elsif local_time > remote_time
156
- puts format_status_message(id, local_item, 'Local')
157
- :remote_update
158
- end
133
+ def update_item_from_hash(db, item)
134
+ fields = "#{ITEM_FIELDS.join(' = ?, ')} = ?"
135
+ db.execute_sql(
136
+ "UPDATE items SET #{fields} WHERE id = ?",
137
+ get_update_values(item)
138
+ )
139
+ end
140
+
141
+ def extract_timestamp(item)
142
+ item['updated_at'].to_i
159
143
  end
144
+ module_function :extract_timestamp
160
145
 
161
- # Converts database items to a hash indexed by ID
162
- #
163
- # @param items [Array<Hash>] Array of database items
164
- # @return [Hash] Items indexed by ID
165
146
  def items_to_hash(items)
166
147
  items.to_h { |item| [item['id'], item] }
167
148
  end
168
149
 
169
- # Determines if remote item should take precedence
170
- #
171
- # @param remote_item [Hash] Remote database item
172
- # @param remote_time [Integer] Remote item timestamp
173
- # @param local_time [Integer] Local item timestamp
174
- # @return [Boolean] true if remote item should take precedence
175
150
  def remote_wins?(remote_item, remote_time, local_time)
176
- remote_time > local_time && (remote_item['deleted'].to_i == 1 || remote_time > local_time)
151
+ time_diff = remote_time > local_time
152
+ time_diff && (remote_item['deleted'].to_i == 1 || time_diff)
177
153
  end
178
154
 
179
- # Formats item status message
180
- #
181
- # @param id [Integer] Item ID
182
- # @param item [Hash] Database item
183
- # @param source [String] Source of the item ('Remote' or 'Local')
184
- # @return [String] Formatted status message
185
155
  def format_status_message(id, item, source)
186
156
  deleted = item['deleted'].to_i == 1 ? ' and deleted' : ''
187
157
  "#{source} item #{id} is newer#{deleted} - #{source == 'Remote' ? 'updating local' : 'will be uploaded'}"
188
158
  end
159
+ module_function :format_status_message
189
160
 
190
- # Updates an existing item in the database with values from a hash
191
- #
192
- # @param db [SQLite3::Database] The database connection
193
- # @param item [Hash] Hash containing item data
194
- # @return [void]
195
- def update_item_from_hash(db, item)
196
- fields = "#{ITEM_FIELDS.join(' = ?, ')} = ?"
197
- db.execute_sql(
198
- "UPDATE items SET #{fields} WHERE id = ?",
199
- get_item_values(item)
200
- )
161
+ def get_insert_values(item)
162
+ @database_fields ||= ITEM_FIELDS
163
+ values = @database_fields.map { |field| item[field] }
164
+ [item['id'], *values]
201
165
  end
202
166
 
203
- # Gets the values array for database operations
204
- #
205
- # @param item [Hash] Hash containing item data
206
- # @param include_id [Boolean] Whether to include ID at start (insert) or end (update)
207
- # @return [Array] Array of values for database operation
208
- def get_item_values(item, include_id_at_start: false)
167
+ def get_update_values(item)
209
168
  @database_fields ||= ITEM_FIELDS
210
- values = @database_fields.map { |field| item[field] }
211
- include_id_at_start ? [item['id'], *values] : values
169
+ @database_fields.map { |field| item[field] }
170
+ end
171
+
172
+ def get_item_values(item, include_id_at_start: false)
173
+ include_id_at_start ? get_insert_values(item) : get_update_values(item)
212
174
  end
213
175
  end
214
176
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'net/http'
2
4
  require 'uri'
3
5
  require 'json'
@@ -83,7 +85,7 @@ module Timet
83
85
  # @return [void]
84
86
  def self.pomodoro_ended(duration)
85
87
  break_duration = (duration / 5).to_i # Assuming a 1/5th break duration
86
- break_duration = 5 if break_duration == 0 # Minimum 5 minute break
88
+ break_duration = 5 if break_duration.zero? # Minimum 5 minute break
87
89
  embed = {
88
90
  title: 'Pomodoro Session Ended! 🎉',
89
91
  description: "Time for a #{break_duration} minute break!",
@@ -5,60 +5,36 @@ require 'logger'
5
5
  require 'dotenv'
6
6
  require 'fileutils'
7
7
 
8
- # The module includes several components:
9
- # - S3 integration for data backup and sync
10
- #
8
+ # Top-level module for the Timet time tracking gem.
11
9
  module Timet
12
- # Required environment variables for S3 configuration
10
+ # Required environment variables for S3 configuration.
13
11
  REQUIRED_ENV_VARS = %w[S3_ENDPOINT S3_ACCESS_KEY S3_SECRET_KEY].freeze
14
12
 
15
- # Ensures that the environment file exists and contains the required variables.
16
- # If the file doesn't exist, it creates it. If required variables are missing,
17
- # it adds them with empty values.
18
- #
19
- # @param env_file_path [String] The path to the environment file
20
- # @return [void]
21
- # @example
22
- # Timet.ensure_env_file_exists('/path/to/.env')
23
13
  def self.ensure_env_file_exists(env_file_path)
14
+ ensure_directory_exists(env_file_path)
15
+ ensure_file_exists(env_file_path)
16
+ append_missing_env_vars(env_file_path)
17
+ end
18
+
19
+ def self.ensure_directory_exists(env_file_path)
24
20
  dir_path = File.dirname(env_file_path)
25
21
  FileUtils.mkdir_p(dir_path)
22
+ end
26
23
 
27
- # Create file if it doesn't exist or is empty
24
+ def self.ensure_file_exists(env_file_path)
28
25
  File.write(env_file_path, '', mode: 'a')
26
+ end
29
27
 
30
- # Load and check environment variables
28
+ def self.append_missing_env_vars(env_file_path)
31
29
  Dotenv.load(env_file_path)
32
30
  missing_vars = REQUIRED_ENV_VARS.reject { |var| ENV.fetch(var, nil) }
33
-
34
- # Append missing variables with empty values
35
31
  return if missing_vars.empty?
36
32
 
37
33
  File.write(env_file_path, "#{missing_vars.map { |var| "#{var}=''" }.join("\n")}\n", mode: 'a')
38
34
  end
39
35
 
40
- # S3Supabase is a class that provides methods to interact with an S3-compatible
41
- # storage service. It encapsulates common operations such as creating a bucket,
42
- # listing objects, uploading and downloading files, deleting objects, and
43
- # deleting a bucket.
44
- #
45
- # This class requires the following environment variables to be set:
46
- # - S3_ENDPOINT: The endpoint URL for the S3-compatible storage service.
47
- # - S3_ACCESS_KEY: The access key ID for authentication.
48
- # - S3_SECRET_KEY: The secret access key for authentication.
49
- # - S3_REGION: The region for the S3-compatible storage service (default: 'us-west-1').
50
- #
51
- # @example Basic usage
52
- # s3_supabase = S3Supabase.new
53
- # s3_supabase.create_bucket('my-bucket')
54
- # s3_supabase.upload_file('my-bucket', '/path/to/local/file.txt', 'file.txt')
55
- #
56
- # @example Advanced operations
57
- # s3_supabase.list_objects('my-bucket')
58
- # s3_supabase.download_file('my-bucket', 'file.txt', '/path/to/download/file.txt')
59
- # s3_supabase.delete_object('my-bucket', 'file.txt')
60
- # s3_supabase.delete_bucket('my-bucket')
61
- class S3Supabase
36
+ # Configuration constants for S3 Supabase integration.
37
+ module S3Config
62
38
  ENV_FILE_PATH = File.join(Dir.home, '.timet', '.env')
63
39
  Timet.ensure_env_file_exists(ENV_FILE_PATH)
64
40
  Dotenv.load(ENV_FILE_PATH)
@@ -68,11 +44,15 @@ module Timet
68
44
  S3_SECRET_KEY = ENV.fetch('S3_SECRET_KEY', nil)
69
45
  S3_REGION = ENV.fetch('S3_REGION', 'us-west-1')
70
46
  LOG_FILE_PATH = File.join(Dir.home, '.timet', 's3_supabase.log')
47
+ end
48
+
49
+ # Struct to hold S3 object reference (bucket name and object key).
50
+ S3ObjectRef = Struct.new(:bucket_name, :object_key, keyword_init: true)
51
+
52
+ # S3Supabase provides methods to interact with an S3-compatible storage service.
53
+ class S3Supabase
54
+ include S3Config
71
55
 
72
- # Initializes a new instance of the S3Supabase class.
73
- # Sets up the AWS S3 client with the configured credentials and endpoint.
74
- #
75
- # @raise [CustomError] If required environment variables are missing
76
56
  def initialize
77
57
  validate_env_vars
78
58
  @logger = Logger.new(LOG_FILE_PATH)
@@ -86,127 +66,92 @@ module Timet
86
66
  )
87
67
  end
88
68
 
89
- # Creates a new bucket in the S3-compatible storage service.
90
- #
91
- # @param bucket_name [String] The name of the bucket to create
92
- # @return [Boolean] true if bucket was created successfully, false otherwise
93
- # @example
94
- # create_bucket('my-new-bucket')
95
69
  def create_bucket(bucket_name)
96
- begin
97
- @s3_client.create_bucket(bucket: bucket_name)
98
- @logger.info "Bucket '#{bucket_name}' created successfully!"
99
- return true
100
- rescue Aws::S3::Errors::BucketAlreadyExists
101
- @logger.error "Error: The bucket '#{bucket_name}' already exists."
102
- rescue Aws::S3::Errors::BucketAlreadyOwnedByYou
103
- @logger.error "Error: The bucket '#{bucket_name}' is already owned by you."
104
- rescue Aws::S3::Errors::ServiceError => e
105
- @logger.error "Error creating bucket: #{e.message}"
106
- end
70
+ @s3_client.create_bucket(bucket: bucket_name)
71
+ log(:info, "Bucket '#{bucket_name}' created successfully!")
72
+ true
73
+ rescue Aws::S3::Errors::BucketAlreadyExists
74
+ log(:error, "Error: The bucket '#{bucket_name}' already exists.")
75
+ false
76
+ rescue Aws::S3::Errors::BucketAlreadyOwnedByYou
77
+ log(:error, "Error: The bucket '#{bucket_name}' is already owned by you.")
78
+ false
79
+ rescue Aws::S3::Errors::ServiceError => e
80
+ log(:error, "Error creating bucket: #{e.message}")
107
81
  false
108
82
  end
109
83
 
110
- # Lists all objects in the specified bucket.
111
- #
112
- # @param bucket_name [String] The name of the bucket to list objects from
113
- # @return [Array<Hash>, false, nil] Array of object hashes if objects found, false if bucket is empty,
114
- # nil if error occurs
115
- # @raise [Aws::S3::Errors::ServiceError] if there's an error accessing the S3 service
116
- # @example
117
- # list_objects('my-bucket') #=> [{key: 'example.txt', last_modified: '2023-01-01', ...}, ...]
118
- # list_objects('empty-bucket') #=> false
119
- # list_objects('invalid-bucket') #=> nil
120
84
  def list_objects(bucket_name)
121
85
  response = @s3_client.list_objects_v2(bucket: bucket_name)
122
- if response.contents.empty?
123
- @logger.info "No objects found in '#{bucket_name}'."
124
- false
125
- else
126
- @logger.info "Objects in '#{bucket_name}':"
127
- response.contents.each { |object| @logger.info "- #{object.key} (Last modified: #{object.last_modified})" }
128
- response.contents.map(&:to_h)
86
+ contents = response.contents
87
+
88
+ if contents.empty?
89
+ log(:error, "No objects found in '#{bucket_name}'.")
90
+ return false
129
91
  end
92
+
93
+ log_object_list(contents, bucket_name)
94
+ contents.map(&:to_h)
130
95
  rescue Aws::S3::Errors::ServiceError => e
131
- @logger.error "Error listing objects: #{e.message}"
96
+ log(:error, "Error listing objects: #{e.message}")
132
97
  nil
133
98
  end
134
99
 
135
- # Uploads a file to the specified bucket.
136
- #
137
- # @param bucket_name [String] The name of the bucket to upload to
138
- # @param file_path [String] The local path of the file to upload
139
- # @param object_key [String] The key (name) to give the object in the bucket
140
- # @return [void]
141
- # @example
142
- # upload_file('my-bucket', '/path/to/local/file.txt', 'remote-file.txt')
143
100
  def upload_file(bucket_name, file_path, object_key)
144
- @s3_client.put_object(
145
- bucket: bucket_name,
146
- key: object_key,
147
- body: File.open(file_path, 'rb')
148
- )
149
- @logger.info "File '#{object_key}' uploaded successfully."
101
+ File.open(file_path, 'rb') do |file|
102
+ @s3_client.put_object(
103
+ bucket: bucket_name,
104
+ key: object_key,
105
+ body: file
106
+ )
107
+ end
108
+ log(:info, "File '#{object_key}' uploaded successfully.")
150
109
  rescue Aws::S3::Errors::ServiceError => e
151
- @logger.error "Error uploading file: #{e.message}"
110
+ log(:error, "Error uploading file: #{e.message}")
152
111
  end
153
112
 
154
- # Downloads a file from the specified bucket.
155
- #
156
- # @param bucket_name [String] The name of the bucket to download from
157
- # @param object_key [String] The key of the object to download
158
- # @param download_path [String] The local path where the file should be saved
159
- # @return [void]
160
- # @example
161
- # download_file('my-bucket', 'remote-file.txt', '/path/to/local/file.txt')
162
113
  def download_file(bucket_name, object_key, download_path)
163
114
  response = @s3_client.get_object(bucket: bucket_name, key: object_key)
164
115
  File.binwrite(download_path, response.body.read)
165
- @logger.info "File '#{object_key}' downloaded successfully."
116
+ log(:info, "File '#{object_key}' downloaded successfully.")
166
117
  rescue Aws::S3::Errors::ServiceError => e
167
- @logger.error "Error downloading file: #{e.message}"
118
+ log(:error, "Error downloading file: #{e.message}")
168
119
  end
169
120
 
170
- # Deletes an object from the specified bucket.
171
- #
172
- # @param bucket_name [String] The name of the bucket containing the object
173
- # @param object_key [String] The key of the object to delete
174
- # @return [void]
175
- # @example
176
- # delete_object('my-bucket', 'file-to-delete.txt')
177
121
  def delete_object(bucket_name, object_key)
178
122
  @s3_client.delete_object(bucket: bucket_name, key: object_key)
179
- @logger.info "Object '#{object_key}' deleted successfully."
123
+ log(:info, "Object '#{object_key}' deleted successfully.")
180
124
  rescue Aws::S3::Errors::ServiceError => e
181
- @logger.error "Error deleting object: #{e.message}"
125
+ log(:error, "Error deleting object: #{e.message}")
182
126
  raise e
183
127
  end
184
128
 
185
- # Deletes a bucket and all its contents.
186
- # First deletes all objects in the bucket, then deletes the bucket itself.
187
- #
188
- # @param bucket_name [String] The name of the bucket to delete
189
- # @return [void]
190
- # @example
191
- # delete_bucket('bucket-to-delete')
192
129
  def delete_bucket(bucket_name)
193
- list_objects(bucket_name)
194
- @s3_client.list_objects_v2(bucket: bucket_name).contents.each do |object|
195
- delete_object(bucket_name, object.key)
196
- end
130
+ delete_all_objects_in_bucket(bucket_name)
197
131
  @s3_client.delete_bucket(bucket: bucket_name)
198
- @logger.info "Bucket '#{bucket_name}' deleted successfully."
132
+ log(:info, "Bucket '#{bucket_name}' deleted successfully.")
199
133
  rescue Aws::S3::Errors::ServiceError => e
200
- @logger.error "Error deleting bucket: #{e.message}"
134
+ log(:error, "Error deleting bucket: #{e.message}")
201
135
  raise e
202
136
  end
203
137
 
204
138
  private
205
139
 
206
- # Validates that all required environment variables are present and non-empty.
207
- #
208
- # @raise [CustomError] If any required environment variables are missing
209
- # @return [void]
140
+ def log(level, message)
141
+ @logger.send(level, message)
142
+ end
143
+
144
+ def log_object_list(contents, bucket_name)
145
+ log(:error, "Objects in '#{bucket_name}':")
146
+ contents.each { |object| log(:error, "- #{object.key} (Last modified: #{object.last_modified})") }
147
+ end
148
+
149
+ def delete_all_objects_in_bucket(bucket_name)
150
+ list_objects(bucket_name)
151
+ contents = @s3_client.list_objects_v2(bucket: bucket_name).contents
152
+ contents.each { |object| delete_object(bucket_name, object.key) }
153
+ end
154
+
210
155
  def validate_env_vars
211
156
  missing_vars = []
212
157
  missing_vars.concat(check_env_var('S3_ENDPOINT', S3_ENDPOINT))
@@ -221,13 +166,11 @@ module Timet
221
166
  def check_env_var(name, value)
222
167
  return [] if value && !value.empty?
223
168
 
169
+ @s3_client&.list_objects_v2(bucket: 'dummy')
224
170
  [name]
225
171
  end
226
172
 
227
173
  # Custom error class that suppresses the backtrace for cleaner error messages.
228
- #
229
- # @example
230
- # raise CustomError, "Missing required environment variables"
231
174
  class CustomError < StandardError
232
175
  def backtrace
233
176
  nil
data/lib/timet/table.rb CHANGED
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # require_relative 'color_codes'
4
- require 'timet/time_report_helper'
5
3
  require_relative 'utils'
6
4
  module Timet
7
5
  # This class is responsible for formatting the output of the `timet` application.
8
6
  # It provides methods for formatting the table header, separators, and rows.
9
7
  class Table
10
- include TimeReportHelper
11
-
12
8
  attr_reader :filter, :items
13
9
 
14
10
  def initialize(filter, items, db)