timet 1.6.2 → 1.6.4

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.
@@ -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)